diff --git a/.coveragerc b/.coveragerc index 99d98d36e6b..6c31546e718 100644 --- a/.coveragerc +++ b/.coveragerc @@ -8,9 +8,6 @@ omit = # omit pieces of code that rely on external devices being present homeassistant/components/acer_projector/* - homeassistant/components/actiontec/const.py - homeassistant/components/actiontec/device_tracker.py - homeassistant/components/actiontec/model.py homeassistant/components/acmeda/__init__.py homeassistant/components/acmeda/base.py homeassistant/components/acmeda/const.py @@ -19,6 +16,9 @@ omit = homeassistant/components/acmeda/helpers.py homeassistant/components/acmeda/hub.py homeassistant/components/acmeda/sensor.py + homeassistant/components/actiontec/const.py + homeassistant/components/actiontec/device_tracker.py + homeassistant/components/actiontec/model.py homeassistant/components/adax/__init__.py homeassistant/components/adax/climate.py homeassistant/components/adguard/__init__.py @@ -62,14 +62,17 @@ omit = homeassistant/components/androidtv/diagnostics.py homeassistant/components/anel_pwrctrl/switch.py homeassistant/components/anthemav/media_player.py - homeassistant/components/apcupsd/* + homeassistant/components/apcupsd/__init__.py + homeassistant/components/apcupsd/binary_sensor.py + homeassistant/components/apcupsd/sensor.py homeassistant/components/apple_tv/__init__.py + homeassistant/components/apple_tv/browse_media.py homeassistant/components/apple_tv/media_player.py homeassistant/components/apple_tv/remote.py homeassistant/components/aqualogic/* homeassistant/components/aquostv/media_player.py - homeassistant/components/arcam_fmj/media_player.py homeassistant/components/arcam_fmj/__init__.py + homeassistant/components/arcam_fmj/media_player.py homeassistant/components/arest/binary_sensor.py homeassistant/components/arest/sensor.py homeassistant/components/arest/switch.py @@ -106,9 +109,9 @@ omit = homeassistant/components/baf/switch.py homeassistant/components/baidu/tts.py homeassistant/components/balboa/__init__.py - homeassistant/components/beewi_smartclim/sensor.py homeassistant/components/bbox/device_tracker.py homeassistant/components/bbox/sensor.py + homeassistant/components/beewi_smartclim/sensor.py homeassistant/components/bitcoin/sensor.py homeassistant/components/bizkaibus/sensor.py homeassistant/components/blink/__init__.py @@ -138,6 +141,7 @@ omit = 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 @@ -152,8 +156,8 @@ omit = homeassistant/components/brottsplatskartan/sensor.py homeassistant/components/browser/* homeassistant/components/brunt/__init__.py - homeassistant/components/brunt/cover.py homeassistant/components/brunt/const.py + homeassistant/components/brunt/cover.py homeassistant/components/bsblan/climate.py homeassistant/components/bt_home_hub_5/device_tracker.py homeassistant/components/bt_smarthub/device_tracker.py @@ -180,20 +184,20 @@ omit = homeassistant/components/concord232/alarm_control_panel.py homeassistant/components/concord232/binary_sensor.py homeassistant/components/control4/__init__.py - homeassistant/components/control4/light.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/listeners.py - homeassistant/components/crownstone/helpers.py homeassistant/components/crownstone/devices.py homeassistant/components/crownstone/entry_manager.py + homeassistant/components/crownstone/helpers.py homeassistant/components/crownstone/light.py + homeassistant/components/crownstone/listeners.py homeassistant/components/cups/sensor.py homeassistant/components/currencylayer/sensor.py homeassistant/components/daikin/__init__.py @@ -236,7 +240,9 @@ omit = homeassistant/components/doorbird/util.py homeassistant/components/dovado/* homeassistant/components/downloader/* - homeassistant/components/dsmr_reader/* + homeassistant/components/dsmr_reader/__init__.py + homeassistant/components/dsmr_reader/definitions.py + homeassistant/components/dsmr_reader/sensor.py homeassistant/components/dte_energy_bridge/sensor.py homeassistant/components/dublin_bus_transport/sensor.py homeassistant/components/dunehd/__init__.py @@ -260,15 +266,15 @@ omit = 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/ecovacs/* - homeassistant/components/edl21/* homeassistant/components/eddystone_temperature/sensor.py homeassistant/components/edimax/switch.py + homeassistant/components/edl21/* homeassistant/components/egardia/* homeassistant/components/eight_sleep/__init__.py homeassistant/components/eight_sleep/binary_sensor.py @@ -284,9 +290,9 @@ omit = homeassistant/components/elkm1/sensor.py homeassistant/components/elkm1/switch.py homeassistant/components/elmax/__init__.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/* homeassistant/components/emby/media_player.py @@ -317,16 +323,17 @@ omit = homeassistant/components/epson/media_player.py homeassistant/components/epsonworkforce/sensor.py homeassistant/components/eq3btsmart/climate.py + homeassistant/components/escea/__init__.py homeassistant/components/escea/climate.py homeassistant/components/escea/discovery.py - homeassistant/components/escea/__init__.py homeassistant/components/esphome/__init__.py homeassistant/components/esphome/binary_sensor.py - homeassistant/components/esphome/bluetooth.py + homeassistant/components/esphome/bluetooth/* homeassistant/components/esphome/button.py homeassistant/components/esphome/camera.py homeassistant/components/esphome/climate.py homeassistant/components/esphome/cover.py + homeassistant/components/esphome/domain_data.py homeassistant/components/esphome/entry_data.py homeassistant/components/esphome/fan.py homeassistant/components/esphome/light.py @@ -341,16 +348,16 @@ omit = homeassistant/components/everlights/light.py homeassistant/components/evohome/* homeassistant/components/ezviz/__init__.py - homeassistant/components/ezviz/camera.py - homeassistant/components/ezviz/coordinator.py - homeassistant/components/ezviz/const.py - homeassistant/components/ezviz/entity.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 homeassistant/components/ezviz/switch.py - homeassistant/components/familyhub/camera.py homeassistant/components/faa_delays/__init__.py homeassistant/components/faa_delays/binary_sensor.py + homeassistant/components/familyhub/camera.py homeassistant/components/fastdotcom/* homeassistant/components/ffmpeg/camera.py homeassistant/components/fibaro/__init__.py @@ -401,9 +408,6 @@ omit = homeassistant/components/flume/coordinator.py homeassistant/components/flume/entity.py homeassistant/components/flume/sensor.py - homeassistant/components/flunearyou/__init__.py - homeassistant/components/flunearyou/repairs.py - homeassistant/components/flunearyou/sensor.py homeassistant/components/folder/sensor.py homeassistant/components/folder_watcher/* homeassistant/components/foobot/sensor.py @@ -423,8 +427,8 @@ omit = homeassistant/components/fritz/services.py homeassistant/components/fritz/switch.py homeassistant/components/fritzbox_callmonitor/__init__.py - homeassistant/components/fritzbox_callmonitor/const.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 @@ -460,8 +464,8 @@ omit = homeassistant/components/gpsd/sensor.py homeassistant/components/greenwave/light.py homeassistant/components/group/notify.py - homeassistant/components/growatt_server/sensor.py homeassistant/components/growatt_server/__init__.py + homeassistant/components/growatt_server/sensor.py homeassistant/components/gstreamer/media_player.py homeassistant/components/gtfs/sensor.py homeassistant/components/guardian/__init__.py @@ -507,9 +511,9 @@ omit = homeassistant/components/home_connect/light.py homeassistant/components/home_connect/sensor.py homeassistant/components/home_connect/switch.py - homeassistant/components/homematic/* homeassistant/components/home_plus_control/api.py homeassistant/components/home_plus_control/switch.py + homeassistant/components/homematic/* homeassistant/components/homeworks/* homeassistant/components/honeywell/__init__.py homeassistant/components/honeywell/climate.py @@ -533,9 +537,9 @@ omit = homeassistant/components/hunterdouglas_powerview/sensor.py homeassistant/components/hunterdouglas_powerview/shade_data.py homeassistant/components/hunterdouglas_powerview/util.py + homeassistant/components/hvv_departures/__init__.py homeassistant/components/hvv_departures/binary_sensor.py homeassistant/components/hvv_departures/sensor.py - homeassistant/components/hvv_departures/__init__.py homeassistant/components/hydrawise/* homeassistant/components/ialarm/alarm_control_panel.py homeassistant/components/iammeter/sensor.py @@ -548,9 +552,6 @@ omit = homeassistant/components/icloud/account.py homeassistant/components/icloud/device_tracker.py homeassistant/components/icloud/sensor.py - homeassistant/components/izone/climate.py - homeassistant/components/izone/discovery.py - homeassistant/components/izone/__init__.py homeassistant/components/idteck_prox/* homeassistant/components/ifttt/__init__.py homeassistant/components/ifttt/alarm_control_panel.py @@ -559,6 +560,7 @@ omit = homeassistant/components/ihc/* 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 @@ -571,13 +573,13 @@ omit = homeassistant/components/insteon/switch.py homeassistant/components/insteon/utils.py homeassistant/components/intellifire/__init__.py - homeassistant/components/intellifire/coordinator.py - homeassistant/components/intellifire/climate.py homeassistant/components/intellifire/binary_sensor.py + homeassistant/components/intellifire/climate.py + homeassistant/components/intellifire/coordinator.py + homeassistant/components/intellifire/entity.py + homeassistant/components/intellifire/fan.py homeassistant/components/intellifire/sensor.py homeassistant/components/intellifire/switch.py - homeassistant/components/intellifire/entity.py - homeassistant/components/incomfort/* homeassistant/components/intesishome/* homeassistant/components/ios/__init__.py homeassistant/components/ios/notify.py @@ -603,6 +605,9 @@ omit = homeassistant/components/isy994/util.py homeassistant/components/itach/remote.py homeassistant/components/itunes/media_player.py + homeassistant/components/izone/__init__.py + homeassistant/components/izone/climate.py + homeassistant/components/izone/discovery.py homeassistant/components/jellyfin/__init__.py homeassistant/components/jellyfin/media_source.py homeassistant/components/joaoapps_join/* @@ -628,6 +633,11 @@ omit = 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 @@ -647,10 +657,6 @@ omit = homeassistant/components/kostal_plenticore/switch.py homeassistant/components/kwb/sensor.py homeassistant/components/lacrosse/sensor.py - homeassistant/components/lametric/__init__.py - homeassistant/components/lametric/button.py - homeassistant/components/lametric/coordinator.py - homeassistant/components/lametric/entity.py homeassistant/components/lametric/notify.py homeassistant/components/lametric/number.py homeassistant/components/lannouncer/notify.py @@ -668,6 +674,9 @@ omit = homeassistant/components/led_ble/util.py homeassistant/components/lg_netcast/media_player.py homeassistant/components/lg_soundbar/media_player.py + homeassistant/components/lidarr/__init__.py + 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 @@ -686,13 +695,13 @@ omit = homeassistant/components/logi_circle/sensor.py homeassistant/components/london_underground/sensor.py homeassistant/components/lookin/__init__.py + homeassistant/components/lookin/climate.py homeassistant/components/lookin/coordinator.py 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/lookin/climate.py - homeassistant/components/lookin/media_player.py - homeassistant/components/lookin/light.py homeassistant/components/luci/device_tracker.py homeassistant/components/luftdaten/sensor.py homeassistant/components/lupusec/* @@ -728,7 +737,6 @@ omit = homeassistant/components/melnor/__init__.py homeassistant/components/melnor/const.py homeassistant/components/melnor/models.py - homeassistant/components/melnor/switch.py homeassistant/components/message_bird/notify.py homeassistant/components/met/weather.py homeassistant/components/met_eireann/__init__.py @@ -778,9 +786,11 @@ omit = homeassistant/components/mullvad/binary_sensor.py homeassistant/components/mutesync/__init__.py homeassistant/components/mutesync/binary_sensor.py - homeassistant/components/nest/const.py homeassistant/components/mvglive/sensor.py homeassistant/components/mycroft/* + homeassistant/components/myq/__init__.py + homeassistant/components/myq/cover.py + homeassistant/components/myq/light.py homeassistant/components/mysensors/__init__.py homeassistant/components/mysensors/binary_sensor.py homeassistant/components/mysensors/climate.py @@ -796,9 +806,6 @@ omit = homeassistant/components/mystrom/binary_sensor.py homeassistant/components/mystrom/light.py homeassistant/components/mystrom/switch.py - homeassistant/components/myq/__init__.py - homeassistant/components/myq/cover.py - homeassistant/components/myq/light.py homeassistant/components/nad/media_player.py homeassistant/components/nanoleaf/__init__.py homeassistant/components/nanoleaf/button.py @@ -814,6 +821,7 @@ 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 @@ -826,36 +834,44 @@ omit = homeassistant/components/netgear_lte/* homeassistant/components/netio/switch.py homeassistant/components/neurio_energy/sensor.py - homeassistant/components/nexia/entity.py homeassistant/components/nexia/climate.py + homeassistant/components/nexia/entity.py homeassistant/components/nexia/switch.py homeassistant/components/nextcloud/* homeassistant/components/nfandroidtv/__init__.py homeassistant/components/nfandroidtv/notify.py + homeassistant/components/nibe_heatpump/__init__.py + homeassistant/components/nibe_heatpump/binary_sensor.py + homeassistant/components/nibe_heatpump/number.py + homeassistant/components/nibe_heatpump/select.py + homeassistant/components/nibe_heatpump/sensor.py + homeassistant/components/nibe_heatpump/switch.py homeassistant/components/niko_home_control/light.py homeassistant/components/nilu/air_quality.py homeassistant/components/nissan_leaf/* homeassistant/components/nmap_tracker/__init__.py homeassistant/components/nmap_tracker/device_tracker.py homeassistant/components/nmbs/sensor.py + homeassistant/components/noaa_tides/sensor.py + homeassistant/components/nobo_hub/__init__.py + homeassistant/components/nobo_hub/climate.py + homeassistant/components/norway_air/air_quality.py + homeassistant/components/notify_events/notify.py homeassistant/components/notion/__init__.py homeassistant/components/notion/binary_sensor.py homeassistant/components/notion/sensor.py - homeassistant/components/noaa_tides/sensor.py - homeassistant/components/norway_air/air_quality.py - homeassistant/components/notify_events/notify.py homeassistant/components/nsw_fuel_station/sensor.py homeassistant/components/nuki/__init__.py - homeassistant/components/nuki/const.py homeassistant/components/nuki/binary_sensor.py + homeassistant/components/nuki/const.py homeassistant/components/nuki/lock.py homeassistant/components/nut/diagnostics.py homeassistant/components/nx584/alarm_control_panel.py homeassistant/components/nzbget/coordinator.py + homeassistant/components/oasa_telematics/sensor.py homeassistant/components/obihai/* homeassistant/components/octoprint/__init__.py homeassistant/components/oem/climate.py - homeassistant/components/oasa_telematics/sensor.py homeassistant/components/ohmconnect/sensor.py homeassistant/components/ombi/* homeassistant/components/omnilogic/__init__.py @@ -889,8 +905,8 @@ omit = homeassistant/components/opengarage/entity.py homeassistant/components/opengarage/sensor.py homeassistant/components/openhome/__init__.py - homeassistant/components/openhome/media_player.py homeassistant/components/openhome/const.py + homeassistant/components/openhome/media_player.py homeassistant/components/opensensemap/air_quality.py homeassistant/components/opensky/sensor.py homeassistant/components/opentherm_gw/__init__.py @@ -915,9 +931,9 @@ omit = homeassistant/components/overkiz/button.py homeassistant/components/overkiz/climate.py homeassistant/components/overkiz/climate_entities/* + homeassistant/components/overkiz/coordinator.py homeassistant/components/overkiz/cover.py homeassistant/components/overkiz/cover_entities/* - homeassistant/components/overkiz/coordinator.py homeassistant/components/overkiz/diagnostics.py homeassistant/components/overkiz/entity.py homeassistant/components/overkiz/executor.py @@ -946,8 +962,8 @@ omit = homeassistant/components/picotts/tts.py homeassistant/components/pilight/* homeassistant/components/ping/__init__.py - homeassistant/components/ping/const.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 @@ -966,13 +982,13 @@ omit = homeassistant/components/point/binary_sensor.py homeassistant/components/point/sensor.py homeassistant/components/poolsense/__init__.py - homeassistant/components/poolsense/sensor.py homeassistant/components/poolsense/binary_sensor.py + homeassistant/components/poolsense/sensor.py homeassistant/components/powerwall/__init__.py - homeassistant/components/proliphix/climate.py homeassistant/components/progettihwsw/__init__.py homeassistant/components/progettihwsw/binary_sensor.py homeassistant/components/progettihwsw/switch.py + homeassistant/components/proliphix/climate.py homeassistant/components/prowl/notify.py homeassistant/components/proxmoxve/* homeassistant/components/proxy/camera.py @@ -993,14 +1009,13 @@ omit = homeassistant/components/rachio/entity.py homeassistant/components/rachio/switch.py homeassistant/components/rachio/webhooks.py - homeassistant/components/radarr/sensor.py homeassistant/components/radio_browser/__init__.py homeassistant/components/radio_browser/media_source.py homeassistant/components/radiotherm/__init__.py - homeassistant/components/radiotherm/entity.py homeassistant/components/radiotherm/climate.py homeassistant/components/radiotherm/coordinator.py homeassistant/components/radiotherm/data.py + homeassistant/components/radiotherm/entity.py homeassistant/components/radiotherm/switch.py homeassistant/components/radiotherm/util.py homeassistant/components/rainbird/* @@ -1009,6 +1024,7 @@ omit = 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 @@ -1021,9 +1037,9 @@ omit = homeassistant/components/reddit/* homeassistant/components/rejseplanen/sensor.py homeassistant/components/remember_the_milk/__init__.py + homeassistant/components/remote_rpi_gpio/* homeassistant/components/repetier/__init__.py homeassistant/components/repetier/sensor.py - homeassistant/components/remote_rpi_gpio/* homeassistant/components/rest/notify.py homeassistant/components/rest/switch.py homeassistant/components/rfxtrx/diagnostics.py @@ -1085,8 +1101,6 @@ omit = homeassistant/components/sesame/lock.py homeassistant/components/seven_segments/image_processing.py homeassistant/components/seventeentrack/sensor.py - homeassistant/components/shiftr/* - homeassistant/components/shodan/sensor.py homeassistant/components/shelly/__init__.py homeassistant/components/shelly/binary_sensor.py homeassistant/components/shelly/climate.py @@ -1095,15 +1109,26 @@ omit = homeassistant/components/shelly/number.py homeassistant/components/shelly/sensor.py homeassistant/components/shelly/utils.py + homeassistant/components/shiftr/* + homeassistant/components/shodan/sensor.py + homeassistant/components/sia/__init__.py + 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 homeassistant/components/simplisafe/alarm_control_panel.py homeassistant/components/simplisafe/binary_sensor.py + 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/* homeassistant/components/skybeacon/sensor.py @@ -1117,15 +1142,9 @@ omit = homeassistant/components/skybell/switch.py homeassistant/components/slack/__init__.py homeassistant/components/slack/notify.py - 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/utils.py - homeassistant/components/sia/sia_entity_base.py - homeassistant/components/sinch/* homeassistant/components/slide/* + homeassistant/components/slimproto/__init__.py + homeassistant/components/slimproto/media_player.py homeassistant/components/sma/__init__.py homeassistant/components/sma/sensor.py homeassistant/components/smappee/__init__.py @@ -1180,8 +1199,6 @@ omit = homeassistant/components/spotify/media_player.py homeassistant/components/spotify/system_health.py homeassistant/components/spotify/util.py - homeassistant/components/slimproto/__init__.py - homeassistant/components/slimproto/media_player.py homeassistant/components/squeezebox/__init__.py homeassistant/components/squeezebox/browse_media.py homeassistant/components/squeezebox/media_player.py @@ -1203,22 +1220,29 @@ omit = homeassistant/components/streamlabswater/* homeassistant/components/suez_water/* homeassistant/components/supervisord/sensor.py + homeassistant/components/supla/* homeassistant/components/surepetcare/__init__.py - homeassistant/components/surepetcare/entity.py homeassistant/components/surepetcare/binary_sensor.py + homeassistant/components/surepetcare/entity.py homeassistant/components/surepetcare/sensor.py homeassistant/components/swiss_hydrological_data/sensor.py homeassistant/components/swiss_public_transport/sensor.py homeassistant/components/swisscom/device_tracker.py - homeassistant/components/switchbot/switch.py - homeassistant/components/switchbot/binary_sensor.py + homeassistant/components/switchbee/__init__.py + homeassistant/components/switchbee/button.py + homeassistant/components/switchbee/coordinator.py + homeassistant/components/switchbee/entity.py + homeassistant/components/switchbee/light.py + homeassistant/components/switchbee/switch.py homeassistant/components/switchbot/__init__.py + homeassistant/components/switchbot/binary_sensor.py homeassistant/components/switchbot/const.py - homeassistant/components/switchbot/entity.py + homeassistant/components/switchbot/coordinator.py homeassistant/components/switchbot/cover.py + homeassistant/components/switchbot/entity.py homeassistant/components/switchbot/light.py homeassistant/components/switchbot/sensor.py - homeassistant/components/switchbot/coordinator.py + homeassistant/components/switchbot/switch.py homeassistant/components/switchmate/switch.py homeassistant/components/syncthing/__init__.py homeassistant/components/syncthing/sensor.py @@ -1230,9 +1254,9 @@ omit = homeassistant/components/synology_dsm/binary_sensor.py homeassistant/components/synology_dsm/button.py 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/common.py homeassistant/components/synology_dsm/entity.py homeassistant/components/synology_dsm/sensor.py homeassistant/components/synology_dsm/service.py @@ -1318,8 +1342,8 @@ omit = homeassistant/components/totalconnect/const.py homeassistant/components/touchline/climate.py homeassistant/components/tplink_lte/* - homeassistant/components/traccar/device_tracker.py 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 @@ -1340,10 +1364,10 @@ omit = homeassistant/components/trafikverket_weatherstation/__init__.py homeassistant/components/trafikverket_weatherstation/coordinator.py homeassistant/components/trafikverket_weatherstation/sensor.py - homeassistant/components/transmission/sensor.py - homeassistant/components/transmission/switch.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 homeassistant/components/tuya/__init__.py homeassistant/components/tuya/alarm_control_panel.py @@ -1372,21 +1396,20 @@ omit = homeassistant/components/ubus/device_tracker.py homeassistant/components/ue_smart_radio/media_player.py homeassistant/components/ukraine_alarm/__init__.py - homeassistant/components/ukraine_alarm/const.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 homeassistant/components/upcloud/switch.py homeassistant/components/upnp/__init__.py homeassistant/components/upnp/device.py homeassistant/components/upnp/sensor.py - homeassistant/components/upc_connect/* - homeassistant/components/uscis/sensor.py homeassistant/components/vallox/__init__.py homeassistant/components/vallox/fan.py homeassistant/components/vallox/sensor.py @@ -1424,17 +1447,17 @@ omit = homeassistant/components/vesync/sensor.py homeassistant/components/vesync/switch.py homeassistant/components/viaggiatreno/sensor.py + homeassistant/components/vicare/__init__.py 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/__init__.py homeassistant/components/vicare/sensor.py homeassistant/components/vicare/water_heater.py homeassistant/components/vilfo/__init__.py - homeassistant/components/vilfo/sensor.py homeassistant/components/vilfo/const.py + homeassistant/components/vilfo/sensor.py homeassistant/components/vivotek/camera.py homeassistant/components/vlc/media_player.py homeassistant/components/vlc_telnet/__init__.py @@ -1467,8 +1490,8 @@ omit = homeassistant/components/wiffi/wiffi_strings.py homeassistant/components/wirelesstag/* homeassistant/components/wolflink/__init__.py - homeassistant/components/wolflink/sensor.py homeassistant/components/wolflink/const.py + homeassistant/components/wolflink/sensor.py homeassistant/components/worldtidesinfo/sensor.py homeassistant/components/worxlandroid/sensor.py homeassistant/components/x10/light.py @@ -1511,12 +1534,6 @@ omit = homeassistant/components/xiaomi_tv/media_player.py homeassistant/components/xmpp/notify.py homeassistant/components/xs1/* - homeassistant/components/yalexs_ble/__init__.py - homeassistant/components/yalexs_ble/binary_sensor.py - homeassistant/components/yalexs_ble/entity.py - homeassistant/components/yalexs_ble/lock.py - homeassistant/components/yalexs_ble/sensor.py - homeassistant/components/yalexs_ble/util.py homeassistant/components/yale_smart_alarm/__init__.py homeassistant/components/yale_smart_alarm/alarm_control_panel.py homeassistant/components/yale_smart_alarm/binary_sensor.py @@ -1526,6 +1543,12 @@ omit = 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 + homeassistant/components/yalexs_ble/binary_sensor.py + homeassistant/components/yalexs_ble/entity.py + homeassistant/components/yalexs_ble/lock.py + homeassistant/components/yalexs_ble/sensor.py + homeassistant/components/yalexs_ble/util.py homeassistant/components/yamaha_musiccast/__init__.py homeassistant/components/yamaha_musiccast/media_player.py homeassistant/components/yamaha_musiccast/number.py @@ -1569,14 +1592,11 @@ omit = homeassistant/components/zhong_hong/climate.py homeassistant/components/ziggo_mediabox_xl/media_player.py homeassistant/components/zoneminder/* - homeassistant/components/supla/* - homeassistant/components/zwave_js/discovery.py - homeassistant/components/zwave_js/sensor.py homeassistant/components/zwave_me/__init__.py homeassistant/components/zwave_me/binary_sensor.py homeassistant/components/zwave_me/button.py - homeassistant/components/zwave_me/cover.py homeassistant/components/zwave_me/climate.py + homeassistant/components/zwave_me/cover.py homeassistant/components/zwave_me/fan.py homeassistant/components/zwave_me/helpers.py homeassistant/components/zwave_me/light.py diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index ac30becb128..5516af1ab4d 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -159,7 +159,7 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Build base image - uses: home-assistant/builder@2022.07.0 + uses: home-assistant/builder@2022.09.0 with: args: | $BUILD_ARGS \ @@ -225,7 +225,7 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Build base image - uses: home-assistant/builder@2022.07.0 + uses: home-assistant/builder@2022.09.0 with: args: | $BUILD_ARGS \ diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 350ba9336a2..7209a0fbf6f 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -22,7 +22,9 @@ on: env: CACHE_VERSION: 1 PIP_CACHE_VERSION: 1 - HA_SHORT_VERSION: 2022.9 + HA_SHORT_VERSION: 2022.10 + # Pin latest Python patch versions to avoid issues + # with runners using different versions. DEFAULT_PYTHON: 3.9.14 ALL_PYTHON_VERSIONS: "['3.9.14', '3.10.7']" PRE_COMMIT_CACHE: ~/.cache/pre-commit @@ -842,9 +844,9 @@ jobs: uses: actions/download-artifact@v3 - name: Upload coverage to Codecov (full coverage) if: needs.info.outputs.test_full_suite == 'true' - uses: codecov/codecov-action@v3.1.0 + uses: codecov/codecov-action@v3.1.1 with: flags: full-suite - name: Upload coverage to Codecov (partial coverage) if: needs.info.outputs.test_full_suite == 'false' - uses: codecov/codecov-action@v3.1.0 + uses: codecov/codecov-action@v3.1.1 diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index d3c30dc6506..f6f7d24fff1 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -17,7 +17,7 @@ jobs: # - No PRs marked as no-stale # - No issues marked as no-stale or help-wanted - name: 90 days stale issues & PRs policy - uses: actions/stale@v5 + uses: actions/stale@v6.0.0 with: repo-token: ${{ secrets.GITHUB_TOKEN }} days-before-stale: 90 @@ -54,7 +54,7 @@ jobs: # - No PRs marked as no-stale or new-integrations # - No issues (-1) - name: 30 days stale PRs policy - uses: actions/stale@v5 + uses: actions/stale@v6.0.0 with: repo-token: ${{ secrets.GITHUB_TOKEN }} days-before-stale: 30 @@ -79,7 +79,7 @@ jobs: # - No Issues marked as no-stale or help-wanted # - No PRs (-1) - name: Needs more information stale issues policy - uses: actions/stale@v5 + uses: actions/stale@v6.0.0 with: repo-token: ${{ secrets.GITHUB_TOKEN }} only-labels: "needs-more-information" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cb9a5ed04dc..088099bf4e4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,17 +1,17 @@ repos: - repo: https://github.com/asottile/pyupgrade - rev: v2.37.3 + rev: v2.38.0 hooks: - id: pyupgrade args: [--py39-plus] - repo: https://github.com/psf/black - rev: 22.6.0 + rev: 22.8.0 hooks: - id: black args: - --safe - --quiet - files: ^((homeassistant|script|tests)/.+)?[^/]+\.py$ + files: ^((homeassistant|pylint|script|tests)/.+)?[^/]+\.py$ - repo: https://github.com/codespell-project/codespell rev: v2.1.0 hooks: @@ -106,7 +106,7 @@ repos: pass_filenames: false language: script types: [text] - files: ^(homeassistant/.+/manifest\.json|pyproject\.toml|\.pre-commit-config\.yaml|script/gen_requirements_all\.py)$ + files: ^(homeassistant/.+/manifest\.json|homeassistant/brands/.+\.json|pyproject\.toml|\.pre-commit-config\.yaml|script/gen_requirements_all\.py)$ - id: hassfest name: hassfest entry: script/run-in-env.sh python3 -m script.hassfest diff --git a/.prettierignore b/.prettierignore index 950741ec8b2..a4d1d99079d 100644 --- a/.prettierignore +++ b/.prettierignore @@ -3,3 +3,4 @@ azure-*.yml docs/source/_templates/* homeassistant/components/*/translations/*.json +homeassistant/generated/* diff --git a/.strict-typing b/.strict-typing index f36439a66d3..cc90af1b98b 100644 --- a/.strict-typing +++ b/.strict-typing @@ -17,6 +17,7 @@ homeassistant.helpers.area_registry homeassistant.helpers.condition homeassistant.helpers.debounce homeassistant.helpers.deprecation +homeassistant.helpers.device_registry homeassistant.helpers.discovery homeassistant.helpers.dispatcher homeassistant.helpers.entity @@ -38,10 +39,9 @@ homeassistant.util.unit_system # --- Add components below this line --- homeassistant.components -homeassistant.components.alert.* homeassistant.components.abode.* -homeassistant.components.acer_projector.* homeassistant.components.accuweather.* +homeassistant.components.acer_projector.* homeassistant.components.actiontec.* homeassistant.components.adguard.* homeassistant.components.aftership.* @@ -51,8 +51,8 @@ homeassistant.components.airvisual.* homeassistant.components.airzone.* homeassistant.components.aladdin_connect.* homeassistant.components.alarm_control_panel.* +homeassistant.components.alert.* homeassistant.components.amazon_polly.* -homeassistant.components.ambee.* homeassistant.components.ambient_station.* homeassistant.components.amcrest.* homeassistant.components.ampio.* @@ -77,9 +77,10 @@ homeassistant.components.calendar.* homeassistant.components.camera.* homeassistant.components.canary.* homeassistant.components.cover.* -homeassistant.components.crownstone.* homeassistant.components.cpuspeed.* +homeassistant.components.crownstone.* homeassistant.components.deconz.* +homeassistant.components.demo.* homeassistant.components.device_automation.* homeassistant.components.device_tracker.* homeassistant.components.devolo_home_control.* @@ -93,8 +94,8 @@ homeassistant.components.efergy.* homeassistant.components.elgato.* homeassistant.components.elkm1.* homeassistant.components.emulated_hue.* -homeassistant.components.esphome.* homeassistant.components.energy.* +homeassistant.components.esphome.* homeassistant.components.evil_genius_labs.* homeassistant.components.fan.* homeassistant.components.fastdotcom.* @@ -102,14 +103,13 @@ homeassistant.components.feedreader.* homeassistant.components.file_upload.* homeassistant.components.filesize.* homeassistant.components.fitbit.* -homeassistant.components.flunearyou.* homeassistant.components.flux_led.* homeassistant.components.forecast_solar.* +homeassistant.components.fritz.* homeassistant.components.fritzbox.* homeassistant.components.fritzbox_callmonitor.* homeassistant.components.fronius.* homeassistant.components.frontend.* -homeassistant.components.fritz.* homeassistant.components.fully_kiosk.* homeassistant.components.geo_location.* homeassistant.components.geocaching.* @@ -144,12 +144,13 @@ homeassistant.components.homewizard.* homeassistant.components.http.* homeassistant.components.huawei_lte.* homeassistant.components.hyperion.* +homeassistant.components.ibeacon.* homeassistant.components.image_processing.* homeassistant.components.input_button.* homeassistant.components.input_select.* homeassistant.components.integration.* -homeassistant.components.isy994.* homeassistant.components.iqvia.* +homeassistant.components.isy994.* homeassistant.components.jellyfin.* homeassistant.components.jewish_calendar.* homeassistant.components.kaleidescape.* @@ -159,8 +160,8 @@ homeassistant.components.lacrosse_view.* homeassistant.components.lametric.* homeassistant.components.laundrify.* homeassistant.components.lcn.* -homeassistant.components.light.* homeassistant.components.lifx.* +homeassistant.components.light.* homeassistant.components.litterrobot.* homeassistant.components.local_ip.* homeassistant.components.lock.* @@ -195,15 +196,15 @@ homeassistant.components.onewire.* homeassistant.components.open_meteo.* homeassistant.components.openexchangerates.* homeassistant.components.openuv.* -homeassistant.components.peco.* homeassistant.components.overkiz.* +homeassistant.components.peco.* homeassistant.components.persistent_notification.* homeassistant.components.pi_hole.* homeassistant.components.powerwall.* homeassistant.components.proximity.* homeassistant.components.prusalink.* -homeassistant.components.pvoutput.* homeassistant.components.pure_energie.* +homeassistant.components.pvoutput.* homeassistant.components.qnap_qsw.* homeassistant.components.rainmachine.* homeassistant.components.rdw.* @@ -212,6 +213,7 @@ homeassistant.components.recorder.* homeassistant.components.remote.* homeassistant.components.renault.* homeassistant.components.repairs.* +homeassistant.components.rfxtrx.* homeassistant.components.rhasspy.* homeassistant.components.ridwell.* homeassistant.components.rituals_perfume_genie.* @@ -222,9 +224,9 @@ homeassistant.components.samsungtv.* homeassistant.components.scene.* homeassistant.components.schedule.* homeassistant.components.select.* +homeassistant.components.senseme.* homeassistant.components.sensibo.* homeassistant.components.sensor.* -homeassistant.components.senseme.* homeassistant.components.senz.* homeassistant.components.shelly.* homeassistant.components.simplisafe.* @@ -232,13 +234,14 @@ homeassistant.components.slack.* homeassistant.components.sleepiq.* homeassistant.components.smhi.* homeassistant.components.ssdp.* -homeassistant.components.stookalert.* homeassistant.components.statistics.* homeassistant.components.steamist.* +homeassistant.components.stookalert.* homeassistant.components.stream.* homeassistant.components.sun.* homeassistant.components.surepetcare.* homeassistant.components.switch.* +homeassistant.components.switchbee.* homeassistant.components.switcher_kis.* homeassistant.components.synology_dsm.* homeassistant.components.systemmonitor.* @@ -247,8 +250,9 @@ homeassistant.components.tailscale.* homeassistant.components.tautulli.* homeassistant.components.tcp.* homeassistant.components.tile.* -homeassistant.components.tplink.* +homeassistant.components.tilt_ble.* homeassistant.components.tolo.* +homeassistant.components.tplink.* homeassistant.components.tractive.* homeassistant.components.tradfri.* homeassistant.components.trafikverket_ferry.* @@ -277,7 +281,7 @@ homeassistant.components.whois.* homeassistant.components.wiz.* homeassistant.components.worldclock.* homeassistant.components.yale_smart_alarm.* -homeassistant.components.zodiac.* homeassistant.components.zeroconf.* +homeassistant.components.zodiac.* homeassistant.components.zone.* homeassistant.components.zwave_js.* diff --git a/CODEOWNERS b/CODEOWNERS index c4706cee1b1..5c39337af74 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -61,8 +61,6 @@ build.json @home-assistant/supervisor /tests/components/alexa/ @home-assistant/cloud @ochlocracy /homeassistant/components/almond/ @gcampax @balloob /tests/components/almond/ @gcampax @balloob -/homeassistant/components/ambee/ @frenck -/tests/components/ambee/ @frenck /homeassistant/components/amberelectric/ @madpilot /tests/components/amberelectric/ @madpilot /homeassistant/components/ambiclimate/ @danielhiversen @@ -80,6 +78,8 @@ build.json @home-assistant/supervisor /tests/components/anthemav/ @hyralex /homeassistant/components/apache_kafka/ @bachya /tests/components/apache_kafka/ @bachya +/homeassistant/components/apcupsd/ @yuxincs +/tests/components/apcupsd/ @yuxincs /homeassistant/components/api/ @home-assistant/core /tests/components/api/ @home-assistant/core /homeassistant/components/apple_tv/ @postlund @@ -129,6 +129,8 @@ build.json @home-assistant/supervisor /tests/components/baf/ @bdraco @jfroy /homeassistant/components/balboa/ @garbled1 /tests/components/balboa/ @garbled1 +/homeassistant/components/bayesian/ @HarvsG +/tests/components/bayesian/ @HarvsG /homeassistant/components/beewi_smartclim/ @alemuro /homeassistant/components/binary_sensor/ @home-assistant/core /tests/components/binary_sensor/ @home-assistant/core @@ -179,8 +181,6 @@ build.json @home-assistant/supervisor /homeassistant/components/cisco_ios/ @fbradyirl /homeassistant/components/cisco_mobility_express/ @fbradyirl /homeassistant/components/cisco_webex_teams/ @fbradyirl -/homeassistant/components/climacell/ @raman325 -/tests/components/climacell/ @raman325 /homeassistant/components/climate/ @home-assistant/core /tests/components/climate/ @home-assistant/core /homeassistant/components/cloud/ @home-assistant/cloud @@ -265,7 +265,8 @@ build.json @home-assistant/supervisor /tests/components/doorbird/ @oblogic7 @bdraco @flacjacket /homeassistant/components/dsmr/ @Robbie1221 @frenck /tests/components/dsmr/ @Robbie1221 @frenck -/homeassistant/components/dsmr_reader/ @depl0y +/homeassistant/components/dsmr_reader/ @depl0y @glodenox +/tests/components/dsmr_reader/ @depl0y @glodenox /homeassistant/components/dunehd/ @bieniu /tests/components/dunehd/ @bieniu /homeassistant/components/dwd_weather_warnings/ @runningman84 @stephan192 @Hummel95 @@ -357,8 +358,6 @@ build.json @home-assistant/supervisor /tests/components/flo/ @dmulcahey /homeassistant/components/flume/ @ChrisMandich @bdraco @jeeftor /tests/components/flume/ @ChrisMandich @bdraco @jeeftor -/homeassistant/components/flunearyou/ @bachya -/tests/components/flunearyou/ @bachya /homeassistant/components/flux_led/ @icemanch @bdraco /tests/components/flux_led/ @icemanch @bdraco /homeassistant/components/forecast_solar/ @klaasnicolaas @frenck @@ -423,6 +422,8 @@ build.json @home-assistant/supervisor /homeassistant/components/google_assistant/ @home-assistant/cloud /tests/components/google_assistant/ @home-assistant/cloud /homeassistant/components/google_cloud/ @lufton +/homeassistant/components/google_sheets/ @tkdrob +/tests/components/google_sheets/ @tkdrob /homeassistant/components/google_travel_time/ @eifinger /tests/components/google_travel_time/ @eifinger /homeassistant/components/govee_ble/ @bdraco @@ -509,6 +510,8 @@ build.json @home-assistant/supervisor /homeassistant/components/iammeter/ @lewei50 /homeassistant/components/iaqualink/ @flz /tests/components/iaqualink/ @flz +/homeassistant/components/ibeacon/ @bdraco +/tests/components/ibeacon/ @bdraco /homeassistant/components/icloud/ @Quentame @nzapponi /tests/components/icloud/ @Quentame @nzapponi /homeassistant/components/ign_sismologia/ @exxamalte @@ -578,7 +581,11 @@ build.json @home-assistant/supervisor /homeassistant/components/keenetic_ndms2/ @foxel /tests/components/keenetic_ndms2/ @foxel /homeassistant/components/kef/ @basnijholt +/homeassistant/components/kegtron/ @Ernst79 +/tests/components/kegtron/ @Ernst79 /homeassistant/components/keyboard_remote/ @bendavid @lanrat +/homeassistant/components/keymitt_ble/ @spycle +/tests/components/keymitt_ble/ @spycle /homeassistant/components/kmtronic/ @dgomes /tests/components/kmtronic/ @dgomes /homeassistant/components/knx/ @Julius2342 @farmio @marvin-w @@ -608,6 +615,8 @@ build.json @home-assistant/supervisor /homeassistant/components/led_ble/ @bdraco /tests/components/led_ble/ @bdraco /homeassistant/components/lg_netcast/ @Drafteed +/homeassistant/components/lidarr/ @tkdrob +/tests/components/lidarr/ @tkdrob /homeassistant/components/life360/ @pnbruckner /tests/components/life360/ @pnbruckner /homeassistant/components/lifx/ @bdraco @Djelibeybi @@ -742,6 +751,8 @@ build.json @home-assistant/supervisor /tests/components/nextdns/ @bieniu /homeassistant/components/nfandroidtv/ @tkdrob /tests/components/nfandroidtv/ @tkdrob +/homeassistant/components/nibe_heatpump/ @elupus +/tests/components/nibe_heatpump/ @elupus /homeassistant/components/nightscout/ @marciogranzotto /tests/components/nightscout/ @marciogranzotto /homeassistant/components/nilu/ @hfurubotten @@ -750,6 +761,8 @@ build.json @home-assistant/supervisor /homeassistant/components/nissan_leaf/ @filcole /homeassistant/components/nmbs/ @thibmaek /homeassistant/components/noaa_tides/ @jdelaney72 +/homeassistant/components/nobo_hub/ @echoromeo @oyvindwe +/tests/components/nobo_hub/ @echoromeo @oyvindwe /homeassistant/components/notify/ @home-assistant/core /tests/components/notify/ @home-assistant/core /homeassistant/components/notify_events/ @matrozov @papajojo @@ -879,6 +892,8 @@ build.json @home-assistant/supervisor /tests/components/qwikswitch/ @kellerza /homeassistant/components/rachio/ @bdraco /tests/components/rachio/ @bdraco +/homeassistant/components/radarr/ @tkdrob +/tests/components/radarr/ @tkdrob /homeassistant/components/radio_browser/ @frenck /tests/components/radio_browser/ @frenck /homeassistant/components/radiotherm/ @bdraco @vinnyfuria @@ -1085,6 +1100,8 @@ build.json @home-assistant/supervisor /tests/components/switch/ @home-assistant/core /homeassistant/components/switch_as_x/ @home-assistant/core /tests/components/switch_as_x/ @home-assistant/core +/homeassistant/components/switchbee/ @jafar-atili +/tests/components/switchbee/ @jafar-atili /homeassistant/components/switchbot/ @bdraco @danielhiversen @RenierM26 @murtas @Eloston /tests/components/switchbot/ @bdraco @danielhiversen @RenierM26 @murtas @Eloston /homeassistant/components/switcher_kis/ @tomerfi @thecode @@ -1130,6 +1147,8 @@ build.json @home-assistant/supervisor /tests/components/tibber/ @danielhiversen /homeassistant/components/tile/ @bachya /tests/components/tile/ @bachya +/homeassistant/components/tilt_ble/ @apt-itude +/tests/components/tilt_ble/ @apt-itude /homeassistant/components/time_date/ @fabaff /tests/components/time_date/ @fabaff /homeassistant/components/tmb/ @alemuro diff --git a/homeassistant/auth/mfa_modules/notify.py b/homeassistant/auth/mfa_modules/notify.py index 1de6c38aecf..0a65c42b520 100644 --- a/homeassistant/auth/mfa_modules/notify.py +++ b/homeassistant/auth/mfa_modules/notify.py @@ -26,7 +26,7 @@ from . import ( SetupFlow, ) -REQUIREMENTS = ["pyotp==2.6.0"] +REQUIREMENTS = ["pyotp==2.7.0"] CONF_MESSAGE = "message" diff --git a/homeassistant/auth/mfa_modules/totp.py b/homeassistant/auth/mfa_modules/totp.py index 397a7fcd386..7db4919ba6f 100644 --- a/homeassistant/auth/mfa_modules/totp.py +++ b/homeassistant/auth/mfa_modules/totp.py @@ -19,7 +19,7 @@ from . import ( SetupFlow, ) -REQUIREMENTS = ["pyotp==2.6.0", "PyQRCode==1.2.1"] +REQUIREMENTS = ["pyotp==2.7.0", "PyQRCode==1.2.1"] CONFIG_SCHEMA = MULTI_FACTOR_AUTH_MODULE_SCHEMA.extend({}, extra=vol.PREVENT_EXTRA) diff --git a/homeassistant/brands/amazon.json b/homeassistant/brands/amazon.json new file mode 100644 index 00000000000..e31bb410457 --- /dev/null +++ b/homeassistant/brands/amazon.json @@ -0,0 +1,5 @@ +{ + "domain": "amazon", + "name": "Amazon", + "integrations": ["alexa", "amazon_polly", "aws", "route53"] +} diff --git a/homeassistant/brands/apple.json b/homeassistant/brands/apple.json new file mode 100644 index 00000000000..00f646e435e --- /dev/null +++ b/homeassistant/brands/apple.json @@ -0,0 +1,12 @@ +{ + "domain": "apple", + "name": "Apple", + "integrations": [ + "apple_tv", + "homekit_controller", + "homekit", + "ibeacon", + "icloud", + "itunes" + ] +} diff --git a/homeassistant/brands/aruba.json b/homeassistant/brands/aruba.json new file mode 100644 index 00000000000..512192813e4 --- /dev/null +++ b/homeassistant/brands/aruba.json @@ -0,0 +1,5 @@ +{ + "domain": "aruba", + "name": "Aruba", + "integrations": ["aruba", "cppm_tracker"] +} diff --git a/homeassistant/brands/asterisk.json b/homeassistant/brands/asterisk.json new file mode 100644 index 00000000000..1df3e660afe --- /dev/null +++ b/homeassistant/brands/asterisk.json @@ -0,0 +1,5 @@ +{ + "domain": "asterisk", + "name": "Asterisk", + "integrations": ["asterisk_cdr", "asterisk_mbox"] +} diff --git a/homeassistant/brands/august.json b/homeassistant/brands/august.json new file mode 100644 index 00000000000..ce2f18dc759 --- /dev/null +++ b/homeassistant/brands/august.json @@ -0,0 +1,5 @@ +{ + "domain": "august", + "name": "August Home", + "integrations": ["august", "yalexs_ble"] +} diff --git a/homeassistant/brands/cisco.json b/homeassistant/brands/cisco.json new file mode 100644 index 00000000000..a1885b1af5e --- /dev/null +++ b/homeassistant/brands/cisco.json @@ -0,0 +1,5 @@ +{ + "domain": "cisco", + "name": "Cisco", + "integrations": ["cisco_ios", "cisco_mobility_express", "cisco_webex_teams"] +} diff --git a/homeassistant/brands/clicksend.json b/homeassistant/brands/clicksend.json new file mode 100644 index 00000000000..07de60a99e3 --- /dev/null +++ b/homeassistant/brands/clicksend.json @@ -0,0 +1,5 @@ +{ + "domain": "clicksend", + "name": "ClickSend", + "integrations": ["clicksend", "clicksend_tts"] +} diff --git a/homeassistant/brands/denon.json b/homeassistant/brands/denon.json new file mode 100644 index 00000000000..a60750e1a31 --- /dev/null +++ b/homeassistant/brands/denon.json @@ -0,0 +1,5 @@ +{ + "domain": "denon", + "name": "Denon", + "integrations": ["denon", "denonavr", "heos"] +} diff --git a/homeassistant/brands/devolo.json b/homeassistant/brands/devolo.json new file mode 100644 index 00000000000..86dc7a3b100 --- /dev/null +++ b/homeassistant/brands/devolo.json @@ -0,0 +1,5 @@ +{ + "domain": "devolo", + "name": "devolo", + "integrations": ["devolo_home_control", "devolo_home_network"] +} diff --git a/homeassistant/brands/dlna.json b/homeassistant/brands/dlna.json new file mode 100644 index 00000000000..f6a648d6895 --- /dev/null +++ b/homeassistant/brands/dlna.json @@ -0,0 +1,5 @@ +{ + "domain": "dlna", + "name": "DLNA", + "integrations": ["dlna_dmr", "dlna_dms"] +} diff --git a/homeassistant/brands/elgato.json b/homeassistant/brands/elgato.json new file mode 100644 index 00000000000..3ca7e07c1bb --- /dev/null +++ b/homeassistant/brands/elgato.json @@ -0,0 +1,5 @@ +{ + "domain": "elgato", + "name": "Elgato", + "integrations": ["avea", "elgato"] +} diff --git a/homeassistant/brands/emoncms.json b/homeassistant/brands/emoncms.json new file mode 100644 index 00000000000..866c7ff18f3 --- /dev/null +++ b/homeassistant/brands/emoncms.json @@ -0,0 +1,5 @@ +{ + "domain": "emoncms", + "name": "emoncms", + "integrations": ["emoncms", "emoncms_history"] +} diff --git a/homeassistant/brands/epson.json b/homeassistant/brands/epson.json new file mode 100644 index 00000000000..80d5db942a2 --- /dev/null +++ b/homeassistant/brands/epson.json @@ -0,0 +1,5 @@ +{ + "domain": "epson", + "name": "Epson", + "integrations": ["epson", "epsonworkforce"] +} diff --git a/homeassistant/brands/eq3.json b/homeassistant/brands/eq3.json new file mode 100644 index 00000000000..4052afac277 --- /dev/null +++ b/homeassistant/brands/eq3.json @@ -0,0 +1,5 @@ +{ + "domain": "eq3", + "name": "eQ-3", + "integrations": ["eq3btsmart", "maxcube"] +} diff --git a/homeassistant/brands/ffmpeg.json b/homeassistant/brands/ffmpeg.json new file mode 100644 index 00000000000..2ec1de4ec03 --- /dev/null +++ b/homeassistant/brands/ffmpeg.json @@ -0,0 +1,5 @@ +{ + "domain": "ffmpeg", + "name": "FFmpeg", + "integrations": ["ffmpeg", "ffmpeg_motion", "ffmpeg_noise"] +} diff --git a/homeassistant/brands/fritzbox.json b/homeassistant/brands/fritzbox.json new file mode 100644 index 00000000000..d0c0d1c1584 --- /dev/null +++ b/homeassistant/brands/fritzbox.json @@ -0,0 +1,5 @@ +{ + "domain": "fritzbox", + "name": "FRITZ!Box", + "integrations": ["fritz", "fritzbox", "fritzbox_callmonitor"] +} diff --git a/homeassistant/brands/geonet.json b/homeassistant/brands/geonet.json new file mode 100644 index 00000000000..4f09d607f80 --- /dev/null +++ b/homeassistant/brands/geonet.json @@ -0,0 +1,5 @@ +{ + "domain": "geonet", + "name": "GeoNet", + "integrations": ["geonetnz_quakes", "geonetnz_volcano"] +} diff --git a/homeassistant/brands/globalcache.json b/homeassistant/brands/globalcache.json new file mode 100644 index 00000000000..0cba9d65d0d --- /dev/null +++ b/homeassistant/brands/globalcache.json @@ -0,0 +1,5 @@ +{ + "domain": "globalcache", + "name": "Global Caché", + "integrations": ["gc100", "itach"] +} diff --git a/homeassistant/brands/google.json b/homeassistant/brands/google.json new file mode 100644 index 00000000000..5f37de46180 --- /dev/null +++ b/homeassistant/brands/google.json @@ -0,0 +1,20 @@ +{ + "domain": "google", + "name": "Google", + "integrations": [ + "google_assistant", + "google_cloud", + "google_domains", + "google_maps", + "google_pubsub", + "google_sheets", + "google_translate", + "google_travel_time", + "google_wifi", + "google", + "nest", + "cast", + "hangouts", + "dialogflow" + ] +} diff --git a/homeassistant/brands/hikvision.json b/homeassistant/brands/hikvision.json new file mode 100644 index 00000000000..b09770bccc5 --- /dev/null +++ b/homeassistant/brands/hikvision.json @@ -0,0 +1,5 @@ +{ + "domain": "hikvision", + "name": "Hikvision", + "integrations": ["hikvision", "hikvisioncam"] +} diff --git a/homeassistant/brands/homematic.json b/homeassistant/brands/homematic.json new file mode 100644 index 00000000000..e7f29c19d67 --- /dev/null +++ b/homeassistant/brands/homematic.json @@ -0,0 +1,5 @@ +{ + "domain": "homematic", + "name": "Homematic", + "integrations": ["homematic", "homematicip_cloud"] +} diff --git a/homeassistant/brands/honeywell.json b/homeassistant/brands/honeywell.json new file mode 100644 index 00000000000..37cd6d8ce73 --- /dev/null +++ b/homeassistant/brands/honeywell.json @@ -0,0 +1,5 @@ +{ + "domain": "honeywell", + "name": "Honeywell", + "integrations": ["lyric", "evohome", "honeywell"] +} diff --git a/homeassistant/brands/ibm.json b/homeassistant/brands/ibm.json new file mode 100644 index 00000000000..42367e899e7 --- /dev/null +++ b/homeassistant/brands/ibm.json @@ -0,0 +1,5 @@ +{ + "domain": "ibm", + "name": "IBM", + "integrations": ["watson_iot", "watson_tts"] +} diff --git a/homeassistant/brands/inovelli.json b/homeassistant/brands/inovelli.json new file mode 100644 index 00000000000..3667a6519c6 --- /dev/null +++ b/homeassistant/brands/inovelli.json @@ -0,0 +1,5 @@ +{ + "domain": "inovelli", + "name": "Inovelli", + "iot_standards": ["zigbee", "zwave"] +} diff --git a/homeassistant/brands/jasco.json b/homeassistant/brands/jasco.json new file mode 100644 index 00000000000..e293b81f994 --- /dev/null +++ b/homeassistant/brands/jasco.json @@ -0,0 +1,5 @@ +{ + "domain": "jasco", + "name": "Jasco", + "iot_standards": ["zwave"] +} diff --git a/homeassistant/brands/leviton.json b/homeassistant/brands/leviton.json new file mode 100644 index 00000000000..b6d78586c1b --- /dev/null +++ b/homeassistant/brands/leviton.json @@ -0,0 +1,5 @@ +{ + "domain": "leviton", + "name": "Leviton", + "iot_standards": ["zwave"] +} diff --git a/homeassistant/brands/lg.json b/homeassistant/brands/lg.json new file mode 100644 index 00000000000..350db80b5f3 --- /dev/null +++ b/homeassistant/brands/lg.json @@ -0,0 +1,5 @@ +{ + "domain": "lg", + "name": "LG", + "integrations": ["lg_netcast", "lg_soundbar", "webostv"] +} diff --git a/homeassistant/brands/logitech.json b/homeassistant/brands/logitech.json new file mode 100644 index 00000000000..d4a0dd1bb87 --- /dev/null +++ b/homeassistant/brands/logitech.json @@ -0,0 +1,5 @@ +{ + "domain": "logitech", + "name": "Logitech", + "integrations": ["harmony", "ue_smart_radio", "squeezebox"] +} diff --git a/homeassistant/brands/lutron.json b/homeassistant/brands/lutron.json new file mode 100644 index 00000000000..b891065d819 --- /dev/null +++ b/homeassistant/brands/lutron.json @@ -0,0 +1,5 @@ +{ + "domain": "lutron", + "name": "Lutron", + "integrations": ["lutron", "lutron_caseta", "homeworks"] +} diff --git a/homeassistant/brands/melnor.json b/homeassistant/brands/melnor.json new file mode 100644 index 00000000000..c04db5c4e7c --- /dev/null +++ b/homeassistant/brands/melnor.json @@ -0,0 +1,5 @@ +{ + "domain": "melnor", + "name": "Melnor", + "integrations": ["melnor", "raincloud"] +} diff --git a/homeassistant/brands/microsoft.json b/homeassistant/brands/microsoft.json new file mode 100644 index 00000000000..d28932082a6 --- /dev/null +++ b/homeassistant/brands/microsoft.json @@ -0,0 +1,16 @@ +{ + "domain": "microsoft", + "name": "Microsoft", + "integrations": [ + "azure_devops", + "azure_event_hub", + "azure_service_bus", + "microsoft_face_detect", + "microsoft_face_identify", + "microsoft_face", + "microsoft", + "msteams", + "xbox", + "xbox_live" + ] +} diff --git a/homeassistant/brands/mqtt.json b/homeassistant/brands/mqtt.json new file mode 100644 index 00000000000..c1d58521a7c --- /dev/null +++ b/homeassistant/brands/mqtt.json @@ -0,0 +1,12 @@ +{ + "domain": "mqtt", + "name": "MQTT", + "integrations": [ + "manual_mqtt", + "mqtt", + "mqtt_eventstream", + "mqtt_json", + "mqtt_room", + "mqtt_statestream" + ] +} diff --git a/homeassistant/brands/netgear.json b/homeassistant/brands/netgear.json new file mode 100644 index 00000000000..9a6b6e51da0 --- /dev/null +++ b/homeassistant/brands/netgear.json @@ -0,0 +1,5 @@ +{ + "domain": "netgear", + "name": "NETGEAR", + "integrations": ["netgear", "netgear_lte"] +} diff --git a/homeassistant/brands/openwrt.json b/homeassistant/brands/openwrt.json new file mode 100644 index 00000000000..ff9cd4ca250 --- /dev/null +++ b/homeassistant/brands/openwrt.json @@ -0,0 +1,5 @@ +{ + "domain": "openwrt", + "name": "OpenWrt", + "integrations": ["luci", "ubus"] +} diff --git a/homeassistant/brands/panasonic.json b/homeassistant/brands/panasonic.json new file mode 100644 index 00000000000..2d8f29a3968 --- /dev/null +++ b/homeassistant/brands/panasonic.json @@ -0,0 +1,5 @@ +{ + "domain": "panasonic", + "name": "Panasonic", + "integrations": ["panasonic_bluray", "panasonic_viera"] +} diff --git a/homeassistant/brands/philips.json b/homeassistant/brands/philips.json new file mode 100644 index 00000000000..bfd290eb945 --- /dev/null +++ b/homeassistant/brands/philips.json @@ -0,0 +1,5 @@ +{ + "domain": "philips", + "name": "Philips", + "integrations": ["dynalite", "hue", "philips_js"] +} diff --git a/homeassistant/brands/qnap.json b/homeassistant/brands/qnap.json new file mode 100644 index 00000000000..6464a0ec877 --- /dev/null +++ b/homeassistant/brands/qnap.json @@ -0,0 +1,5 @@ +{ + "domain": "qnap", + "name": "QNAP", + "integrations": ["qnap", "qnap_qsw"] +} diff --git a/homeassistant/brands/raspberry.json b/homeassistant/brands/raspberry.json new file mode 100644 index 00000000000..a0ec6f12699 --- /dev/null +++ b/homeassistant/brands/raspberry.json @@ -0,0 +1,5 @@ +{ + "domain": "raspberry_pi", + "name": "Raspberry Pi", + "integrations": ["rpi_camera", "rpi_power", "remote_rpi_gpio"] +} diff --git a/homeassistant/brands/russound.json b/homeassistant/brands/russound.json new file mode 100644 index 00000000000..70b3de109ca --- /dev/null +++ b/homeassistant/brands/russound.json @@ -0,0 +1,5 @@ +{ + "domain": "russound", + "name": "Russound", + "integrations": ["russound_rio", "russound_rnet"] +} diff --git a/homeassistant/brands/samsung.json b/homeassistant/brands/samsung.json new file mode 100644 index 00000000000..1d5f2522e9e --- /dev/null +++ b/homeassistant/brands/samsung.json @@ -0,0 +1,5 @@ +{ + "domain": "samsung", + "name": "Samsung", + "integrations": ["familyhub", "samsungtv", "syncthru"] +} diff --git a/homeassistant/brands/solaredge.json b/homeassistant/brands/solaredge.json new file mode 100644 index 00000000000..90190f9c786 --- /dev/null +++ b/homeassistant/brands/solaredge.json @@ -0,0 +1,5 @@ +{ + "domain": "solaredge", + "name": "SolarEdge", + "integrations": ["solaredge", "solaredge_local"] +} diff --git a/homeassistant/brands/sony.json b/homeassistant/brands/sony.json new file mode 100644 index 00000000000..e35d5f4723c --- /dev/null +++ b/homeassistant/brands/sony.json @@ -0,0 +1,5 @@ +{ + "domain": "sony", + "name": "Sony", + "integrations": ["braviatv", "ps4", "sony_projector", "songpal"] +} diff --git a/homeassistant/brands/synology.json b/homeassistant/brands/synology.json new file mode 100644 index 00000000000..0387fabffaf --- /dev/null +++ b/homeassistant/brands/synology.json @@ -0,0 +1,5 @@ +{ + "domain": "synology", + "name": "Synology", + "integrations": ["synology_chat", "synology_dsm", "synology_srm"] +} diff --git a/homeassistant/brands/telegram.json b/homeassistant/brands/telegram.json new file mode 100644 index 00000000000..8cb5e202190 --- /dev/null +++ b/homeassistant/brands/telegram.json @@ -0,0 +1,5 @@ +{ + "domain": "telegram", + "name": "Telegram", + "integrations": ["telegram", "telegram_bot"] +} diff --git a/homeassistant/brands/telldus.json b/homeassistant/brands/telldus.json new file mode 100644 index 00000000000..c280832f68e --- /dev/null +++ b/homeassistant/brands/telldus.json @@ -0,0 +1,5 @@ +{ + "domain": "telldus", + "name": "Telldus", + "integrations": ["tellduslive", "tellstick"] +} diff --git a/homeassistant/brands/tesla.json b/homeassistant/brands/tesla.json new file mode 100644 index 00000000000..aeec7982579 --- /dev/null +++ b/homeassistant/brands/tesla.json @@ -0,0 +1,5 @@ +{ + "domain": "tesla", + "name": "Tesla", + "integrations": ["powerwall", "tesla_wall_connector"] +} diff --git a/homeassistant/brands/third_reality.json b/homeassistant/brands/third_reality.json new file mode 100644 index 00000000000..172b74c42fc --- /dev/null +++ b/homeassistant/brands/third_reality.json @@ -0,0 +1,5 @@ +{ + "domain": "third_reality", + "name": "Third Reality", + "iot_standards": ["zigbee"] +} diff --git a/homeassistant/brands/trafikverket.json b/homeassistant/brands/trafikverket.json new file mode 100644 index 00000000000..df444cbeb60 --- /dev/null +++ b/homeassistant/brands/trafikverket.json @@ -0,0 +1,9 @@ +{ + "domain": "trafikverket", + "name": "Trafikverket", + "integrations": [ + "trafikverket_ferry", + "trafikverket_train", + "trafikverket_weatherstation" + ] +} diff --git a/homeassistant/brands/twilio.json b/homeassistant/brands/twilio.json new file mode 100644 index 00000000000..7ae9162059e --- /dev/null +++ b/homeassistant/brands/twilio.json @@ -0,0 +1,5 @@ +{ + "domain": "twilio", + "name": "Twilio", + "integrations": ["twilio", "twilio_call", "twilio_sms"] +} diff --git a/homeassistant/brands/u_tec.json b/homeassistant/brands/u_tec.json new file mode 100644 index 00000000000..2ce4be9a7d9 --- /dev/null +++ b/homeassistant/brands/u_tec.json @@ -0,0 +1,5 @@ +{ + "domain": "u_tec", + "name": "U-tec", + "iot_standards": ["zwave"] +} diff --git a/homeassistant/brands/ubiquiti.json b/homeassistant/brands/ubiquiti.json new file mode 100644 index 00000000000..8b64cffaa7e --- /dev/null +++ b/homeassistant/brands/ubiquiti.json @@ -0,0 +1,5 @@ +{ + "domain": "ubiquiti", + "name": "Ubiquiti", + "integrations": ["unifi", "unifi_direct", "unifiled", "unifiprotect"] +} diff --git a/homeassistant/brands/vlc.json b/homeassistant/brands/vlc.json new file mode 100644 index 00000000000..66c004470d6 --- /dev/null +++ b/homeassistant/brands/vlc.json @@ -0,0 +1,5 @@ +{ + "domain": "vlc", + "name": "VideoLAN", + "integrations": ["vlc", "vlc_telnet"] +} diff --git a/homeassistant/brands/xiaomi.json b/homeassistant/brands/xiaomi.json new file mode 100644 index 00000000000..ebdc99d8c38 --- /dev/null +++ b/homeassistant/brands/xiaomi.json @@ -0,0 +1,11 @@ +{ + "domain": "xiaomi", + "name": "Xiaomi", + "integrations": [ + "xiaomi_aqara", + "xiaomi_ble", + "xiaomi_miio", + "xiaomi_tv", + "xiaomi" + ] +} diff --git a/homeassistant/brands/yale.json b/homeassistant/brands/yale.json new file mode 100644 index 00000000000..87c119fdd40 --- /dev/null +++ b/homeassistant/brands/yale.json @@ -0,0 +1,5 @@ +{ + "domain": "yale", + "name": "Yale", + "integrations": ["august", "yale_smart_alarm", "yalexs_ble"] +} diff --git a/homeassistant/brands/yandex.json b/homeassistant/brands/yandex.json new file mode 100644 index 00000000000..c4a55be8b5e --- /dev/null +++ b/homeassistant/brands/yandex.json @@ -0,0 +1,5 @@ +{ + "domain": "yandex", + "name": "Yandex", + "integrations": ["yandex_transport", "yandextts"] +} diff --git a/homeassistant/brands/yeelight.json b/homeassistant/brands/yeelight.json new file mode 100644 index 00000000000..1ce04a99214 --- /dev/null +++ b/homeassistant/brands/yeelight.json @@ -0,0 +1,5 @@ +{ + "domain": "yeelight", + "name": "Yeelight", + "integrations": ["yeelight", "yeelightsunflower"] +} diff --git a/homeassistant/brands/zooz.json b/homeassistant/brands/zooz.json new file mode 100644 index 00000000000..f3032e58653 --- /dev/null +++ b/homeassistant/brands/zooz.json @@ -0,0 +1,5 @@ +{ + "domain": "zooz", + "name": "Zooz", + "iot_standards": ["zwave"] +} diff --git a/homeassistant/components/abode/translations/es.json b/homeassistant/components/abode/translations/es.json index c7db5e8db6a..6a9e70c1363 100644 --- a/homeassistant/components/abode/translations/es.json +++ b/homeassistant/components/abode/translations/es.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente", "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." }, "error": { diff --git a/homeassistant/components/abode/translations/pt.json b/homeassistant/components/abode/translations/pt.json index 3d6b007b471..7dc8448b288 100644 --- a/homeassistant/components/abode/translations/pt.json +++ b/homeassistant/components/abode/translations/pt.json @@ -6,20 +6,29 @@ }, "error": { "cannot_connect": "Falha na liga\u00e7\u00e3o", - "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "invalid_mfa_code": "C\u00f3digo MFA inv\u00e1lido" }, "step": { + "mfa": { + "data": { + "mfa_code": "C\u00f3digo MFA (6 d\u00edgitos)" + }, + "title": "Introduza seu c\u00f3digo MFA para Abode" + }, "reauth_confirm": { "data": { "password": "Palavra-passe", "username": "Email" - } + }, + "title": "Preencha as informa\u00e7\u00f5es de login de Abode" }, "user": { "data": { "password": "Palavra-passe", "username": "Email" - } + }, + "title": "Preencha as informa\u00e7\u00f5es de login de Abode" } } } diff --git a/homeassistant/components/accuweather/translations/bg.json b/homeassistant/components/accuweather/translations/bg.json index 26fdf8e85d5..6cd4cdde80e 100644 --- a/homeassistant/components/accuweather/translations/bg.json +++ b/homeassistant/components/accuweather/translations/bg.json @@ -17,5 +17,14 @@ } } } + }, + "options": { + "step": { + "user": { + "data": { + "forecast": "\u041f\u0440\u043e\u0433\u043d\u043e\u0437\u0430 \u0437\u0430 \u0432\u0440\u0435\u043c\u0435\u0442\u043e" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/accuweather/translations/pt.json b/homeassistant/components/accuweather/translations/pt.json index 8b5d307e722..1346ee8367f 100644 --- a/homeassistant/components/accuweather/translations/pt.json +++ b/homeassistant/components/accuweather/translations/pt.json @@ -8,7 +8,8 @@ }, "error": { "cannot_connect": "Falha na liga\u00e7\u00e3o", - "invalid_api_key": "Chave de API inv\u00e1lida" + "invalid_api_key": "Chave de API inv\u00e1lida", + "requests_exceeded": "O n\u00famero permitido de pedidos \u00e0 API do Accuweather foi excedido. \u00c9 necess\u00e1rio aguardar ou alterar a chave API." }, "step": { "user": { @@ -26,8 +27,15 @@ "user": { "data": { "forecast": "Previs\u00e3o meteorol\u00f3gica" - } + }, + "description": "Devido \u00e0s limita\u00e7\u00f5es da vers\u00e3o gratuita da chave AccuWeather API, quando se activa a previs\u00e3o do tempo, as actualiza\u00e7\u00f5es de dados ser\u00e3o realizadas a cada 80 minutos em vez de a cada 40 minutos." } } + }, + "system_health": { + "info": { + "can_reach_server": "Alcance o servidor AccuWeather", + "remaining_requests": "Pedidos permitidos restantes" + } } } \ No newline at end of file diff --git a/homeassistant/components/accuweather/translations/sensor.pt.json b/homeassistant/components/accuweather/translations/sensor.pt.json new file mode 100644 index 00000000000..9d4d6573509 --- /dev/null +++ b/homeassistant/components/accuweather/translations/sensor.pt.json @@ -0,0 +1,9 @@ +{ + "state": { + "accuweather__pressure_tendency": { + "falling": "A decrescer", + "rising": "A aumentar", + "steady": "Est\u00e1vel" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/acmeda/translations/pt.json b/homeassistant/components/acmeda/translations/pt.json index 8fcd9c13425..4eb37998165 100644 --- a/homeassistant/components/acmeda/translations/pt.json +++ b/homeassistant/components/acmeda/translations/pt.json @@ -2,6 +2,13 @@ "config": { "abort": { "no_devices_found": "Nenhum dispositivo encontrado na rede" + }, + "step": { + "user": { + "data": { + "id": "ID do anfitri\u00e3o" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/actiontec/device_tracker.py b/homeassistant/components/actiontec/device_tracker.py index cc26c191c8c..9c18e2ba907 100644 --- a/homeassistant/components/actiontec/device_tracker.py +++ b/homeassistant/components/actiontec/device_tracker.py @@ -31,7 +31,9 @@ PLATFORM_SCHEMA: Final = BASE_PLATFORM_SCHEMA.extend( ) -def get_scanner(hass: HomeAssistant, config: ConfigType) -> DeviceScanner | None: +def get_scanner( + hass: HomeAssistant, config: ConfigType +) -> ActiontecDeviceScanner | None: """Validate the configuration and return an Actiontec scanner.""" scanner = ActiontecDeviceScanner(config[DOMAIN]) return scanner if scanner.success_init else None diff --git a/homeassistant/components/adax/climate.py b/homeassistant/components/adax/climate.py index 85532d9aadb..8703619ca92 100644 --- a/homeassistant/components/adax/climate.py +++ b/homeassistant/components/adax/climate.py @@ -6,8 +6,11 @@ from typing import Any from adax import Adax from adax_local import Adax as AdaxLocal -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ClimateEntityFeature, HVACMode +from homeassistant.components.climate import ( + ClimateEntity, + ClimateEntityFeature, + HVACMode, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_TEMPERATURE, diff --git a/homeassistant/components/adax/translations/pt.json b/homeassistant/components/adax/translations/pt.json index b83b23758af..b8256278f36 100644 --- a/homeassistant/components/adax/translations/pt.json +++ b/homeassistant/components/adax/translations/pt.json @@ -1,6 +1,9 @@ { "config": { "abort": { + "already_configured": "DIspositivo j\u00e1 est\u00e1 configurado", + "heater_not_available": "Aquecedor n\u00e3o dispon\u00edvel. Tente reiniciar o aquecedor premindo + e OK durante alguns segundos.", + "heater_not_found": "Aquecedor n\u00e3o encontrado. Tente mover o aquecedor para mais perto do computador com Home Assistant.", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" }, "error": { @@ -9,8 +12,22 @@ "step": { "cloud": { "data": { + "account_id": "ID da conta", "password": "Palavra-passe" } + }, + "local": { + "data": { + "wifi_pswd": "Senha Wi-Fi", + "wifi_ssid": "Wi-Fi SSID" + }, + "description": "Reiniciar o aquecedor premindo + e OK at\u00e9 a visualiza\u00e7\u00e3o mostrar 'Reiniciar'. Depois premir e manter premido o bot\u00e3o OK no aquecedor at\u00e9 que o led azul comece a piscar antes de premir Submeter. A configura\u00e7\u00e3o do aquecedor pode demorar alguns minutos." + }, + "user": { + "data": { + "connection_type": "Selecione o tipo de liga\u00e7\u00e3o" + }, + "description": "Selecione o tipo de liga\u00e7\u00e3o. 'Local' requer aquecedores com bluetooth" } } } diff --git a/homeassistant/components/adguard/translations/pt.json b/homeassistant/components/adguard/translations/pt.json index e389261748d..28a63ea54e7 100644 --- a/homeassistant/components/adguard/translations/pt.json +++ b/homeassistant/components/adguard/translations/pt.json @@ -8,7 +8,8 @@ }, "step": { "hassio_confirm": { - "title": "AdGuard Home via Supervisor add-on" + "description": "Deseja configurar o Home Assistant para se ligar ao AdGuard Home fornecido pelo add-on: {addon}?", + "title": "AdGuard Home via add-on Supervisor" }, "user": { "data": { diff --git a/homeassistant/components/advantage_air/__init__.py b/homeassistant/components/advantage_air/__init__.py index 3e07a8fbcef..8620a228edf 100644 --- a/homeassistant/components/advantage_air/__init__.py +++ b/homeassistant/components/advantage_air/__init__.py @@ -7,6 +7,7 @@ from advantage_air import ApiError, advantage_air from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_IP_ADDRESS, CONF_PORT, Platform from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -60,7 +61,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if await func(param): await coordinator.async_refresh() except ApiError as err: - _LOGGER.warning(err) + raise HomeAssistantError(err) from err return error_handle @@ -69,8 +70,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = { "coordinator": coordinator, - "async_change": error_handle_factory(api.aircon.async_set), - "async_set_light": error_handle_factory(api.lights.async_set), + "aircon": error_handle_factory(api.aircon.async_set), + "lights": error_handle_factory(api.lights.async_set), } await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) diff --git a/homeassistant/components/advantage_air/binary_sensor.py b/homeassistant/components/advantage_air/binary_sensor.py index c0934239fe7..f3e021855fa 100644 --- a/homeassistant/components/advantage_air/binary_sensor.py +++ b/homeassistant/components/advantage_air/binary_sensor.py @@ -1,6 +1,8 @@ """Binary Sensor platform for Advantage Air integration.""" from __future__ import annotations +from typing import Any + from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, BinarySensorEntity, @@ -26,15 +28,16 @@ async def async_setup_entry( instance = hass.data[ADVANTAGE_AIR_DOMAIN][config_entry.entry_id] entities: list[BinarySensorEntity] = [] - for ac_key, ac_device in instance["coordinator"].data["aircons"].items(): - entities.append(AdvantageAirFilter(instance, ac_key)) - for zone_key, zone in ac_device["zones"].items(): - # Only add motion sensor when motion is enabled - if zone["motionConfig"] >= 2: - entities.append(AdvantageAirZoneMotion(instance, ac_key, zone_key)) - # Only add MyZone if it is available - if zone["type"] != 0: - entities.append(AdvantageAirZoneMyZone(instance, ac_key, zone_key)) + if aircons := instance["coordinator"].data.get("aircons"): + for ac_key, ac_device in aircons.items(): + entities.append(AdvantageAirFilter(instance, ac_key)) + for zone_key, zone in ac_device["zones"].items(): + # Only add motion sensor when motion is enabled + if zone["motionConfig"] >= 2: + entities.append(AdvantageAirZoneMotion(instance, ac_key, zone_key)) + # Only add MyZone if it is available + if zone["type"] != 0: + entities.append(AdvantageAirZoneMyZone(instance, ac_key, zone_key)) async_add_entities(entities) @@ -45,13 +48,13 @@ class AdvantageAirFilter(AdvantageAirAcEntity, BinarySensorEntity): _attr_entity_category = EntityCategory.DIAGNOSTIC _attr_name = "Filter" - def __init__(self, instance, ac_key): + def __init__(self, instance: dict[str, Any], ac_key: str) -> None: """Initialize an Advantage Air Filter sensor.""" super().__init__(instance, ac_key) self._attr_unique_id += "-filter" @property - def is_on(self): + def is_on(self) -> bool: """Return if filter needs cleaning.""" return self._ac["filterCleanStatus"] @@ -61,14 +64,14 @@ class AdvantageAirZoneMotion(AdvantageAirZoneEntity, BinarySensorEntity): _attr_device_class = BinarySensorDeviceClass.MOTION - def __init__(self, instance, ac_key, zone_key): + def __init__(self, instance: dict[str, Any], ac_key: str, zone_key: str) -> None: """Initialize an Advantage Air Zone Motion sensor.""" super().__init__(instance, ac_key, zone_key) self._attr_name = f'{self._zone["name"]} motion' self._attr_unique_id += "-motion" @property - def is_on(self): + def is_on(self) -> bool: """Return if motion is detect.""" return self._zone["motion"] == 20 @@ -79,13 +82,13 @@ class AdvantageAirZoneMyZone(AdvantageAirZoneEntity, BinarySensorEntity): _attr_entity_registry_enabled_default = False _attr_entity_category = EntityCategory.DIAGNOSTIC - def __init__(self, instance, ac_key, zone_key): + def __init__(self, instance: dict[str, Any], ac_key: str, zone_key: str) -> None: """Initialize an Advantage Air Zone MyZone sensor.""" super().__init__(instance, ac_key, zone_key) self._attr_name = f'{self._zone["name"]} myZone' self._attr_unique_id += "-myzone" @property - def is_on(self): + def is_on(self) -> bool: """Return if this zone is the myZone.""" return self._zone["number"] == self._ac["myZone"] diff --git a/homeassistant/components/advantage_air/climate.py b/homeassistant/components/advantage_air/climate.py index c11b01f3ace..fdba46cde76 100644 --- a/homeassistant/components/advantage_air/climate.py +++ b/homeassistant/components/advantage_air/climate.py @@ -4,12 +4,12 @@ from __future__ import annotations import logging from typing import Any -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( FAN_AUTO, FAN_HIGH, FAN_LOW, FAN_MEDIUM, + ClimateEntity, ClimateEntityFeature, HVACMode, ) @@ -70,12 +70,13 @@ async def async_setup_entry( instance = hass.data[ADVANTAGE_AIR_DOMAIN][config_entry.entry_id] entities: list[ClimateEntity] = [] - for ac_key, ac_device in instance["coordinator"].data["aircons"].items(): - entities.append(AdvantageAirAC(instance, ac_key)) - for zone_key, zone in ac_device["zones"].items(): - # Only add zone climate control when zone is in temperature control - if zone["type"] != 0: - entities.append(AdvantageAirZone(instance, ac_key, zone_key)) + if aircons := instance["coordinator"].data.get("aircons"): + for ac_key, ac_device in aircons.items(): + entities.append(AdvantageAirAC(instance, ac_key)) + for zone_key, zone in ac_device["zones"].items(): + # Only add zone climate control when zone is in temperature control + if zone["type"] != 0: + entities.append(AdvantageAirZone(instance, ac_key, zone_key)) async_add_entities(entities) @@ -92,37 +93,37 @@ class AdvantageAirAC(AdvantageAirAcEntity, ClimateEntity): ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE ) - def __init__(self, instance, ac_key): + def __init__(self, instance: dict[str, Any], ac_key: str) -> None: """Initialize an AdvantageAir AC unit.""" super().__init__(instance, ac_key) if self._ac.get("myAutoModeEnabled"): self._attr_hvac_modes = AC_HVAC_MODES + [HVACMode.AUTO] @property - def target_temperature(self): + def target_temperature(self) -> float: """Return the current target temperature.""" return self._ac["setTemp"] @property - def hvac_mode(self): + def hvac_mode(self) -> HVACMode | None: """Return the current HVAC modes.""" if self._ac["state"] == ADVANTAGE_AIR_STATE_ON: return ADVANTAGE_AIR_HVAC_MODES.get(self._ac["mode"]) return HVACMode.OFF @property - def fan_mode(self): + def fan_mode(self) -> str | None: """Return the current fan modes.""" return ADVANTAGE_AIR_FAN_MODES.get(self._ac["fan"]) async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set the HVAC Mode and State.""" if hvac_mode == HVACMode.OFF: - await self.async_change( + await self.aircon( {self.ac_key: {"info": {"state": ADVANTAGE_AIR_STATE_OFF}}} ) else: - await self.async_change( + await self.aircon( { self.ac_key: { "info": { @@ -135,14 +136,14 @@ class AdvantageAirAC(AdvantageAirAcEntity, ClimateEntity): async def async_set_fan_mode(self, fan_mode: str) -> None: """Set the Fan Mode.""" - await self.async_change( + await self.aircon( {self.ac_key: {"info": {"fan": HASS_FAN_MODES.get(fan_mode)}}} ) async def async_set_temperature(self, **kwargs: Any) -> None: """Set the Temperature.""" temp = kwargs.get(ATTR_TEMPERATURE) - await self.async_change({self.ac_key: {"info": {"setTemp": temp}}}) + await self.aircon({self.ac_key: {"info": {"setTemp": temp}}}) class AdvantageAirZone(AdvantageAirZoneEntity, ClimateEntity): @@ -155,7 +156,7 @@ class AdvantageAirZone(AdvantageAirZoneEntity, ClimateEntity): _attr_hvac_modes = ZONE_HVAC_MODES _attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE - def __init__(self, instance, ac_key, zone_key): + def __init__(self, instance: dict[str, Any], ac_key: str, zone_key: str) -> None: """Initialize an AdvantageAir Zone control.""" super().__init__(instance, ac_key, zone_key) self._attr_name = self._zone["name"] @@ -164,26 +165,26 @@ class AdvantageAirZone(AdvantageAirZoneEntity, ClimateEntity): ) @property - def hvac_mode(self): + def hvac_mode(self) -> HVACMode: """Return the current state as HVAC mode.""" if self._zone["state"] == ADVANTAGE_AIR_STATE_OPEN: return HVACMode.HEAT_COOL return HVACMode.OFF @property - def current_temperature(self): + def current_temperature(self) -> float: """Return the current temperature.""" return self._zone["measuredTemp"] @property - def target_temperature(self): + def target_temperature(self) -> float: """Return the target temperature.""" return self._zone["setTemp"] async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set the HVAC Mode and State.""" if hvac_mode == HVACMode.OFF: - await self.async_change( + await self.aircon( { self.ac_key: { "zones": {self.zone_key: {"state": ADVANTAGE_AIR_STATE_CLOSE}} @@ -191,7 +192,7 @@ class AdvantageAirZone(AdvantageAirZoneEntity, ClimateEntity): } ) else: - await self.async_change( + await self.aircon( { self.ac_key: { "zones": {self.zone_key: {"state": ADVANTAGE_AIR_STATE_OPEN}} @@ -202,6 +203,4 @@ class AdvantageAirZone(AdvantageAirZoneEntity, ClimateEntity): async def async_set_temperature(self, **kwargs: Any) -> None: """Set the Temperature.""" temp = kwargs.get(ATTR_TEMPERATURE) - await self.async_change( - {self.ac_key: {"zones": {self.zone_key: {"setTemp": temp}}}} - ) + await self.aircon({self.ac_key: {"zones": {self.zone_key: {"setTemp": temp}}}}) diff --git a/homeassistant/components/advantage_air/config_flow.py b/homeassistant/components/advantage_air/config_flow.py index b13ab1e9b21..7b5acab55f0 100644 --- a/homeassistant/components/advantage_air/config_flow.py +++ b/homeassistant/components/advantage_air/config_flow.py @@ -1,9 +1,14 @@ """Config Flow for Advantage Air integration.""" +from __future__ import annotations + +from typing import Any + from advantage_air import ApiError, advantage_air import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_IP_ADDRESS, CONF_PORT +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import ADVANTAGE_AIR_RETRY, DOMAIN @@ -25,7 +30,9 @@ class AdvantageAirConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): DOMAIN = DOMAIN - async def async_step_user(self, user_input=None): + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Get configuration from the user.""" errors = {} if user_input: diff --git a/homeassistant/components/advantage_air/cover.py b/homeassistant/components/advantage_air/cover.py index 4b3f371f52e..8d05f7e2e63 100644 --- a/homeassistant/components/advantage_air/cover.py +++ b/homeassistant/components/advantage_air/cover.py @@ -30,12 +30,13 @@ async def async_setup_entry( instance = hass.data[ADVANTAGE_AIR_DOMAIN][config_entry.entry_id] - entities = [] - for ac_key, ac_device in instance["coordinator"].data["aircons"].items(): - for zone_key, zone in ac_device["zones"].items(): - # Only add zone vent controls when zone in vent control mode. - if zone["type"] == 0: - entities.append(AdvantageAirZoneVent(instance, ac_key, zone_key)) + entities: list[CoverEntity] = [] + if aircons := instance["coordinator"].data.get("aircons"): + for ac_key, ac_device in aircons.items(): + for zone_key, zone in ac_device["zones"].items(): + # Only add zone vent controls when zone in vent control mode. + if zone["type"] == 0: + entities.append(AdvantageAirZoneVent(instance, ac_key, zone_key)) async_add_entities(entities) @@ -49,7 +50,7 @@ class AdvantageAirZoneVent(AdvantageAirZoneEntity, CoverEntity): | CoverEntityFeature.SET_POSITION ) - def __init__(self, instance, ac_key, zone_key): + def __init__(self, instance: dict[str, Any], ac_key: str, zone_key: str) -> None: """Initialize an Advantage Air Zone Vent.""" super().__init__(instance, ac_key, zone_key) self._attr_name = self._zone["name"] @@ -68,7 +69,7 @@ class AdvantageAirZoneVent(AdvantageAirZoneEntity, CoverEntity): async def async_open_cover(self, **kwargs: Any) -> None: """Fully open zone vent.""" - await self.async_change( + await self.aircon( { self.ac_key: { "zones": { @@ -80,7 +81,7 @@ class AdvantageAirZoneVent(AdvantageAirZoneEntity, CoverEntity): async def async_close_cover(self, **kwargs: Any) -> None: """Fully close zone vent.""" - await self.async_change( + await self.aircon( { self.ac_key: { "zones": {self.zone_key: {"state": ADVANTAGE_AIR_STATE_CLOSE}} @@ -92,7 +93,7 @@ class AdvantageAirZoneVent(AdvantageAirZoneEntity, CoverEntity): """Change vent position.""" position = round(kwargs[ATTR_POSITION] / 5) * 5 if position == 0: - await self.async_change( + await self.aircon( { self.ac_key: { "zones": {self.zone_key: {"state": ADVANTAGE_AIR_STATE_CLOSE}} @@ -100,7 +101,7 @@ class AdvantageAirZoneVent(AdvantageAirZoneEntity, CoverEntity): } ) else: - await self.async_change( + await self.aircon( { self.ac_key: { "zones": { diff --git a/homeassistant/components/advantage_air/entity.py b/homeassistant/components/advantage_air/entity.py index 375bfa255c4..aaaa4ff5813 100644 --- a/homeassistant/components/advantage_air/entity.py +++ b/homeassistant/components/advantage_air/entity.py @@ -1,5 +1,7 @@ """Advantage Air parent entity class.""" +from typing import Any + from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -11,20 +13,20 @@ class AdvantageAirEntity(CoordinatorEntity): _attr_has_entity_name = True - def __init__(self, instance): + def __init__(self, instance: dict[str, Any]) -> None: """Initialize common aspects of an Advantage Air entity.""" super().__init__(instance["coordinator"]) - self._attr_unique_id = self.coordinator.data["system"]["rid"] + self._attr_unique_id: str = self.coordinator.data["system"]["rid"] class AdvantageAirAcEntity(AdvantageAirEntity): """Parent class for Advantage Air AC Entities.""" - def __init__(self, instance, ac_key): + def __init__(self, instance: dict[str, Any], ac_key: str) -> None: """Initialize common aspects of an Advantage Air ac entity.""" super().__init__(instance) - self.async_change = instance["async_change"] - self.ac_key = ac_key + self.aircon = instance["aircon"] + self.ac_key: str = ac_key self._attr_unique_id += f"-{ac_key}" self._attr_device_info = DeviceInfo( @@ -36,19 +38,19 @@ class AdvantageAirAcEntity(AdvantageAirEntity): ) @property - def _ac(self): + def _ac(self) -> dict[str, Any]: return self.coordinator.data["aircons"][self.ac_key]["info"] class AdvantageAirZoneEntity(AdvantageAirAcEntity): """Parent class for Advantage Air Zone Entities.""" - def __init__(self, instance, ac_key, zone_key): + def __init__(self, instance: dict[str, Any], ac_key: str, zone_key: str) -> None: """Initialize common aspects of an Advantage Air zone entity.""" super().__init__(instance, ac_key) - self.zone_key = zone_key + self.zone_key: str = zone_key self._attr_unique_id += f"-{zone_key}" @property - def _zone(self): + def _zone(self) -> dict[str, Any]: return self.coordinator.data["aircons"][self.ac_key]["zones"][self.zone_key] diff --git a/homeassistant/components/advantage_air/light.py b/homeassistant/components/advantage_air/light.py index b1c8495edf8..f0ae669acde 100644 --- a/homeassistant/components/advantage_air/light.py +++ b/homeassistant/components/advantage_air/light.py @@ -24,9 +24,9 @@ async def async_setup_entry( instance = hass.data[ADVANTAGE_AIR_DOMAIN][config_entry.entry_id] - entities = [] - if "myLights" in instance["coordinator"].data: - for light in instance["coordinator"].data["myLights"]["lights"].values(): + entities: list[LightEntity] = [] + if my_lights := instance["coordinator"].data.get("myLights"): + for light in my_lights["lights"].values(): if light.get("relay"): entities.append(AdvantageAirLight(instance, light)) else: @@ -39,11 +39,11 @@ class AdvantageAirLight(AdvantageAirEntity, LightEntity): _attr_supported_color_modes = {ColorMode.ONOFF} - def __init__(self, instance, light): + def __init__(self, instance: dict[str, Any], light: dict[str, Any]) -> None: """Initialize an Advantage Air Light.""" super().__init__(instance) - self.async_set_light = instance["async_set_light"] - self._id = light["id"] + self.lights = instance["lights"] + self._id: str = light["id"] self._attr_unique_id += f"-{self._id}" self._attr_device_info = DeviceInfo( identifiers={(ADVANTAGE_AIR_DOMAIN, self._attr_unique_id)}, @@ -54,7 +54,7 @@ class AdvantageAirLight(AdvantageAirEntity, LightEntity): ) @property - def _light(self): + def _light(self) -> dict[str, Any]: """Return the light object.""" return self.coordinator.data["myLights"]["lights"][self._id] @@ -65,11 +65,11 @@ class AdvantageAirLight(AdvantageAirEntity, LightEntity): async def async_turn_on(self, **kwargs: Any) -> None: """Turn the light on.""" - await self.async_set_light({"id": self._id, "state": ADVANTAGE_AIR_STATE_ON}) + await self.lights({"id": self._id, "state": ADVANTAGE_AIR_STATE_ON}) async def async_turn_off(self, **kwargs: Any) -> None: """Turn the light off.""" - await self.async_set_light({"id": self._id, "state": ADVANTAGE_AIR_STATE_OFF}) + await self.lights({"id": self._id, "state": ADVANTAGE_AIR_STATE_OFF}) class AdvantageAirLightDimmable(AdvantageAirLight): @@ -84,7 +84,7 @@ class AdvantageAirLightDimmable(AdvantageAirLight): async def async_turn_on(self, **kwargs: Any) -> None: """Turn the light on and optionally set the brightness.""" - data = {"id": self._id, "state": ADVANTAGE_AIR_STATE_ON} + data: dict[str, Any] = {"id": self._id, "state": ADVANTAGE_AIR_STATE_ON} if ATTR_BRIGHTNESS in kwargs: data["value"] = round(kwargs[ATTR_BRIGHTNESS] * 100 / 255) - await self.async_set_light(data) + await self.lights(data) diff --git a/homeassistant/components/advantage_air/select.py b/homeassistant/components/advantage_air/select.py index f6c46cb7f87..742ce810011 100644 --- a/homeassistant/components/advantage_air/select.py +++ b/homeassistant/components/advantage_air/select.py @@ -1,4 +1,6 @@ """Select platform for Advantage Air integration.""" +from typing import Any + from homeassistant.components.select import SelectEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -19,9 +21,10 @@ async def async_setup_entry( instance = hass.data[ADVANTAGE_AIR_DOMAIN][config_entry.entry_id] - entities = [] - for ac_key in instance["coordinator"].data["aircons"]: - entities.append(AdvantageAirMyZone(instance, ac_key)) + entities: list[SelectEntity] = [] + if aircons := instance["coordinator"].data.get("aircons"): + for ac_key in aircons: + entities.append(AdvantageAirMyZone(instance, ac_key)) async_add_entities(entities) @@ -31,7 +34,7 @@ class AdvantageAirMyZone(AdvantageAirAcEntity, SelectEntity): _attr_icon = "mdi:home-thermometer" _attr_name = "MyZone" - def __init__(self, instance, ac_key): + def __init__(self, instance: dict[str, Any], ac_key: str) -> None: """Initialize an Advantage Air MyZone control.""" super().__init__(instance, ac_key) self._attr_unique_id += "-myzone" @@ -52,6 +55,6 @@ class AdvantageAirMyZone(AdvantageAirAcEntity, SelectEntity): async def async_select_option(self, option: str) -> None: """Set the MyZone.""" - await self.async_change( + await self.aircon( {self.ac_key: {"info": {"myZone": self._name_to_number[option]}}} ) diff --git a/homeassistant/components/advantage_air/sensor.py b/homeassistant/components/advantage_air/sensor.py index b110294b2fd..60e640d36e9 100644 --- a/homeassistant/components/advantage_air/sensor.py +++ b/homeassistant/components/advantage_air/sensor.py @@ -1,6 +1,9 @@ """Sensor platform for Advantage Air integration.""" from __future__ import annotations +from decimal import Decimal +from typing import Any + import voluptuous as vol from homeassistant.components.sensor import ( @@ -35,17 +38,18 @@ async def async_setup_entry( instance = hass.data[ADVANTAGE_AIR_DOMAIN][config_entry.entry_id] entities: list[SensorEntity] = [] - for ac_key, ac_device in instance["coordinator"].data["aircons"].items(): - entities.append(AdvantageAirTimeTo(instance, ac_key, "On")) - entities.append(AdvantageAirTimeTo(instance, ac_key, "Off")) - for zone_key, zone in ac_device["zones"].items(): - # Only show damper and temp sensors when zone is in temperature control - if zone["type"] != 0: - entities.append(AdvantageAirZoneVent(instance, ac_key, zone_key)) - entities.append(AdvantageAirZoneTemp(instance, ac_key, zone_key)) - # Only show wireless signal strength sensors when using wireless sensors - if zone["rssi"] > 0: - entities.append(AdvantageAirZoneSignal(instance, ac_key, zone_key)) + if aircons := instance["coordinator"].data.get("aircons"): + for ac_key, ac_device in aircons.items(): + entities.append(AdvantageAirTimeTo(instance, ac_key, "On")) + entities.append(AdvantageAirTimeTo(instance, ac_key, "Off")) + for zone_key, zone in ac_device["zones"].items(): + # Only show damper and temp sensors when zone is in temperature control + if zone["type"] != 0: + entities.append(AdvantageAirZoneVent(instance, ac_key, zone_key)) + entities.append(AdvantageAirZoneTemp(instance, ac_key, zone_key)) + # Only show wireless signal strength sensors when using wireless sensors + if zone["rssi"] > 0: + entities.append(AdvantageAirZoneSignal(instance, ac_key, zone_key)) async_add_entities(entities) platform = entity_platform.async_get_current_platform() @@ -62,7 +66,7 @@ class AdvantageAirTimeTo(AdvantageAirAcEntity, SensorEntity): _attr_native_unit_of_measurement = ADVANTAGE_AIR_SET_COUNTDOWN_UNIT _attr_entity_category = EntityCategory.DIAGNOSTIC - def __init__(self, instance, ac_key, action): + def __init__(self, instance: dict[str, Any], ac_key: str, action: str) -> None: """Initialize the Advantage Air timer control.""" super().__init__(instance, ac_key) self.action = action @@ -71,21 +75,21 @@ class AdvantageAirTimeTo(AdvantageAirAcEntity, SensorEntity): self._attr_unique_id += f"-timeto{action}" @property - def native_value(self): + def native_value(self) -> Decimal: """Return the current value.""" return self._ac[self._time_key] @property - def icon(self): + def icon(self) -> str: """Return a representative icon of the timer.""" if self._ac[self._time_key] > 0: return "mdi:timer-outline" return "mdi:timer-off-outline" - async def set_time_to(self, **kwargs): + async def set_time_to(self, **kwargs: Any) -> None: """Set the timer value.""" value = min(720, max(0, int(kwargs[ADVANTAGE_AIR_SET_COUNTDOWN_VALUE]))) - await self.async_change({self.ac_key: {"info": {self._time_key: value}}}) + await self.aircon({self.ac_key: {"info": {self._time_key: value}}}) class AdvantageAirZoneVent(AdvantageAirZoneEntity, SensorEntity): @@ -95,21 +99,21 @@ class AdvantageAirZoneVent(AdvantageAirZoneEntity, SensorEntity): _attr_state_class = SensorStateClass.MEASUREMENT _attr_entity_category = EntityCategory.DIAGNOSTIC - def __init__(self, instance, ac_key, zone_key): + def __init__(self, instance: dict[str, Any], ac_key: str, zone_key: str) -> None: """Initialize an Advantage Air Zone Vent Sensor.""" super().__init__(instance, ac_key, zone_key=zone_key) self._attr_name = f'{self._zone["name"]} vent' self._attr_unique_id += "-vent" @property - def native_value(self): + def native_value(self) -> Decimal: """Return the current value of the air vent.""" if self._zone["state"] == ADVANTAGE_AIR_STATE_OPEN: return self._zone["value"] - return 0 + return Decimal(0) @property - def icon(self): + def icon(self) -> str: """Return a representative icon.""" if self._zone["state"] == ADVANTAGE_AIR_STATE_OPEN: return "mdi:fan" @@ -123,19 +127,19 @@ class AdvantageAirZoneSignal(AdvantageAirZoneEntity, SensorEntity): _attr_state_class = SensorStateClass.MEASUREMENT _attr_entity_category = EntityCategory.DIAGNOSTIC - def __init__(self, instance, ac_key, zone_key): + def __init__(self, instance: dict[str, Any], ac_key: str, zone_key: str) -> None: """Initialize an Advantage Air Zone wireless signal sensor.""" super().__init__(instance, ac_key, zone_key) self._attr_name = f'{self._zone["name"]} signal' self._attr_unique_id += "-signal" @property - def native_value(self): + def native_value(self) -> Decimal: """Return the current value of the wireless signal.""" return self._zone["rssi"] @property - def icon(self): + def icon(self) -> str: """Return a representative icon.""" if self._zone["rssi"] >= 80: return "mdi:wifi-strength-4" @@ -157,13 +161,13 @@ class AdvantageAirZoneTemp(AdvantageAirZoneEntity, SensorEntity): _attr_entity_registry_enabled_default = False _attr_entity_category = EntityCategory.DIAGNOSTIC - def __init__(self, instance, ac_key, zone_key): + def __init__(self, instance: dict[str, Any], ac_key: str, zone_key: str) -> None: """Initialize an Advantage Air Zone Temp Sensor.""" super().__init__(instance, ac_key, zone_key) self._attr_name = f'{self._zone["name"]} temperature' self._attr_unique_id += "-temp" @property - def native_value(self): + def native_value(self) -> Decimal: """Return the current value of the measured temperature.""" return self._zone["measuredTemp"] diff --git a/homeassistant/components/advantage_air/switch.py b/homeassistant/components/advantage_air/switch.py index 52992ae9531..e3504ab7624 100644 --- a/homeassistant/components/advantage_air/switch.py +++ b/homeassistant/components/advantage_air/switch.py @@ -23,10 +23,11 @@ async def async_setup_entry( instance = hass.data[ADVANTAGE_AIR_DOMAIN][config_entry.entry_id] - entities = [] - for ac_key, ac_device in instance["coordinator"].data["aircons"].items(): - if ac_device["info"]["freshAirStatus"] != "none": - entities.append(AdvantageAirFreshAir(instance, ac_key)) + entities: list[SwitchEntity] = [] + if aircons := instance["coordinator"].data.get("aircons"): + for ac_key, ac_device in aircons.items(): + if ac_device["info"]["freshAirStatus"] != "none": + entities.append(AdvantageAirFreshAir(instance, ac_key)) async_add_entities(entities) @@ -36,24 +37,24 @@ class AdvantageAirFreshAir(AdvantageAirAcEntity, SwitchEntity): _attr_icon = "mdi:air-filter" _attr_name = "Fresh air" - def __init__(self, instance, ac_key): + def __init__(self, instance: dict[str, Any], ac_key: str) -> None: """Initialize an Advantage Air fresh air control.""" super().__init__(instance, ac_key) self._attr_unique_id += "-freshair" @property - def is_on(self): + def is_on(self) -> bool: """Return the fresh air status.""" return self._ac["freshAirStatus"] == ADVANTAGE_AIR_STATE_ON async def async_turn_on(self, **kwargs: Any) -> None: """Turn fresh air on.""" - await self.async_change( + await self.aircon( {self.ac_key: {"info": {"freshAirStatus": ADVANTAGE_AIR_STATE_ON}}} ) async def async_turn_off(self, **kwargs: Any) -> None: """Turn fresh air off.""" - await self.async_change( + await self.aircon( {self.ac_key: {"info": {"freshAirStatus": ADVANTAGE_AIR_STATE_OFF}}} ) diff --git a/homeassistant/components/advantage_air/update.py b/homeassistant/components/advantage_air/update.py index 9294ecab238..404fcad7447 100644 --- a/homeassistant/components/advantage_air/update.py +++ b/homeassistant/components/advantage_air/update.py @@ -1,4 +1,6 @@ """Advantage Air Update platform.""" +from typing import Any + from homeassistant.components.update import UpdateEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -26,7 +28,7 @@ class AdvantageAirApp(AdvantageAirEntity, UpdateEntity): _attr_name = "App" - def __init__(self, instance): + def __init__(self, instance: dict[str, Any]) -> None: """Initialize the Advantage Air App.""" super().__init__(instance) self._attr_device_info = DeviceInfo( @@ -40,12 +42,12 @@ class AdvantageAirApp(AdvantageAirEntity, UpdateEntity): ) @property - def installed_version(self): + def installed_version(self) -> str: """Return the current app version.""" return self.coordinator.data["system"]["myAppRev"] @property - def latest_version(self): + def latest_version(self) -> str: """Return if there is an update.""" if self.coordinator.data["system"]["needsUpdate"]: return "Needs Update" diff --git a/homeassistant/components/aemet/translations/ja.json b/homeassistant/components/aemet/translations/ja.json index 1279f90beaa..343284ac6d8 100644 --- a/homeassistant/components/aemet/translations/ja.json +++ b/homeassistant/components/aemet/translations/ja.json @@ -14,7 +14,7 @@ "longitude": "\u7d4c\u5ea6", "name": "\u7d71\u5408\u306e\u540d\u524d" }, - "description": "AEMET OpenData\u7d71\u5408\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002 API\u30ad\u30fc\u3092\u751f\u6210\u3059\u308b\u306b\u306f\u3001https://opendata.aemet.es/centrodedescargas/altaUsuario \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044" + "description": "API \u30ad\u30fc\u3092\u751f\u6210\u3059\u308b\u306b\u306f\u3001https://opendata.aemet.es/centrodedescargas/altaUsuario \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044" } } }, diff --git a/homeassistant/components/aemet/translations/pt.json b/homeassistant/components/aemet/translations/pt.json index cc227afe3a3..6b89c6c3015 100644 --- a/homeassistant/components/aemet/translations/pt.json +++ b/homeassistant/components/aemet/translations/pt.json @@ -1,7 +1,30 @@ { "config": { + "abort": { + "already_configured": "A localiza\u00e7\u00e3o j\u00e1 est\u00e1 configurada" + }, "error": { "invalid_api_key": "Chave de API inv\u00e1lida" + }, + "step": { + "user": { + "data": { + "api_key": "Chave da API", + "latitude": "Latitude", + "longitude": "Longitude", + "name": "Nome da integra\u00e7\u00e3o" + }, + "description": "Para gerar a chave API v\u00e1 a https://opendata.aemet.es/centrodedescargas/altaUsuario" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "station_updates": "Recolha de dados das esta\u00e7\u00f5es meteorol\u00f3gicas AEMET" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/air_quality/__init__.py b/homeassistant/components/air_quality/__init__.py index d0aa1fd4a76..c2992cc804b 100644 --- a/homeassistant/components/air_quality/__init__.py +++ b/homeassistant/components/air_quality/__init__.py @@ -50,10 +50,12 @@ PROP_TO_ATTR: Final[dict[str, str]] = { "sulphur_dioxide": ATTR_SO2, } +# mypy: disallow-any-generics + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the air quality component.""" - component = hass.data[DOMAIN] = EntityComponent( + component = hass.data[DOMAIN] = EntityComponent[AirQualityEntity]( _LOGGER, DOMAIN, hass, SCAN_INTERVAL ) await component.async_setup(config) @@ -62,13 +64,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry.""" - component: EntityComponent = hass.data[DOMAIN] + component: EntityComponent[AirQualityEntity] = hass.data[DOMAIN] return await component.async_setup_entry(entry) async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - component: EntityComponent = hass.data[DOMAIN] + component: EntityComponent[AirQualityEntity] = hass.data[DOMAIN] return await component.async_unload_entry(entry) diff --git a/homeassistant/components/air_quality/manifest.json b/homeassistant/components/air_quality/manifest.json index 978089d1816..587e9809e76 100644 --- a/homeassistant/components/air_quality/manifest.json +++ b/homeassistant/components/air_quality/manifest.json @@ -3,5 +3,6 @@ "name": "Air Quality", "documentation": "https://www.home-assistant.io/integrations/air_quality", "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "entity" } diff --git a/homeassistant/components/airly/const.py b/homeassistant/components/airly/const.py index 8fddaea8ec2..76260699dbd 100644 --- a/homeassistant/components/airly/const.py +++ b/homeassistant/components/airly/const.py @@ -7,11 +7,15 @@ ATTR_API_ADVICE: Final = "ADVICE" ATTR_API_CAQI: Final = "CAQI" ATTR_API_CAQI_DESCRIPTION: Final = "DESCRIPTION" ATTR_API_CAQI_LEVEL: Final = "LEVEL" +ATTR_API_CO: Final = "CO" ATTR_API_HUMIDITY: Final = "HUMIDITY" +ATTR_API_NO2: Final = "NO2" +ATTR_API_O3: Final = "O3" ATTR_API_PM10: Final = "PM10" ATTR_API_PM1: Final = "PM1" ATTR_API_PM25: Final = "PM25" ATTR_API_PRESSURE: Final = "PRESSURE" +ATTR_API_SO2: Final = "SO2" ATTR_API_TEMPERATURE: Final = "TEMPERATURE" ATTR_ADVICE: Final = "advice" diff --git a/homeassistant/components/airly/sensor.py b/homeassistant/components/airly/sensor.py index a1c9f8a3057..122990adecc 100644 --- a/homeassistant/components/airly/sensor.py +++ b/homeassistant/components/airly/sensor.py @@ -33,11 +33,15 @@ from .const import ( ATTR_API_CAQI, ATTR_API_CAQI_DESCRIPTION, ATTR_API_CAQI_LEVEL, + ATTR_API_CO, ATTR_API_HUMIDITY, + ATTR_API_NO2, + ATTR_API_O3, ATTR_API_PM1, ATTR_API_PM10, ATTR_API_PM25, ATTR_API_PRESSURE, + ATTR_API_SO2, ATTR_API_TEMPERATURE, ATTR_DESCRIPTION, ATTR_LEVEL, @@ -64,7 +68,7 @@ class AirlySensorEntityDescription(SensorEntityDescription): SENSOR_TYPES: tuple[AirlySensorEntityDescription, ...] = ( AirlySensorEntityDescription( key=ATTR_API_CAQI, - device_class=SensorDeviceClass.AQI, + icon="mdi:air-filter", name=ATTR_API_CAQI, native_unit_of_measurement="CAQI", ), @@ -112,6 +116,33 @@ SENSOR_TYPES: tuple[AirlySensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, value=lambda value: round(value, 1), ), + AirlySensorEntityDescription( + key=ATTR_API_CO, + name=ATTR_API_CO, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + state_class=SensorStateClass.MEASUREMENT, + ), + AirlySensorEntityDescription( + key=ATTR_API_NO2, + device_class=SensorDeviceClass.NITROGEN_DIOXIDE, + name=ATTR_API_NO2, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + state_class=SensorStateClass.MEASUREMENT, + ), + AirlySensorEntityDescription( + key=ATTR_API_SO2, + device_class=SensorDeviceClass.SULPHUR_DIOXIDE, + name=ATTR_API_SO2, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + state_class=SensorStateClass.MEASUREMENT, + ), + AirlySensorEntityDescription( + key=ATTR_API_O3, + device_class=SensorDeviceClass.OZONE, + name=ATTR_API_O3, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + state_class=SensorStateClass.MEASUREMENT, + ), ) @@ -191,4 +222,32 @@ class AirlySensor(CoordinatorEntity[AirlyDataUpdateCoordinator], SensorEntity): self._attrs[ATTR_PERCENT] = round( self.coordinator.data[f"{ATTR_API_PM10}_{SUFFIX_PERCENT}"] ) + if self.entity_description.key == ATTR_API_CO: + self._attrs[ATTR_LIMIT] = self.coordinator.data[ + f"{ATTR_API_CO}_{SUFFIX_LIMIT}" + ] + self._attrs[ATTR_PERCENT] = round( + self.coordinator.data[f"{ATTR_API_CO}_{SUFFIX_PERCENT}"] + ) + if self.entity_description.key == ATTR_API_NO2: + self._attrs[ATTR_LIMIT] = self.coordinator.data[ + f"{ATTR_API_NO2}_{SUFFIX_LIMIT}" + ] + self._attrs[ATTR_PERCENT] = round( + self.coordinator.data[f"{ATTR_API_NO2}_{SUFFIX_PERCENT}"] + ) + if self.entity_description.key == ATTR_API_SO2: + self._attrs[ATTR_LIMIT] = self.coordinator.data[ + f"{ATTR_API_SO2}_{SUFFIX_LIMIT}" + ] + self._attrs[ATTR_PERCENT] = round( + self.coordinator.data[f"{ATTR_API_SO2}_{SUFFIX_PERCENT}"] + ) + if self.entity_description.key == ATTR_API_O3: + self._attrs[ATTR_LIMIT] = self.coordinator.data[ + f"{ATTR_API_O3}_{SUFFIX_LIMIT}" + ] + self._attrs[ATTR_PERCENT] = round( + self.coordinator.data[f"{ATTR_API_O3}_{SUFFIX_PERCENT}"] + ) return self._attrs diff --git a/homeassistant/components/airly/translations/pt.json b/homeassistant/components/airly/translations/pt.json index aff2cb2399b..03b6ca72520 100644 --- a/homeassistant/components/airly/translations/pt.json +++ b/homeassistant/components/airly/translations/pt.json @@ -13,7 +13,8 @@ "latitude": "Latitude", "longitude": "Longitude", "name": "Nome" - } + }, + "description": "Para gerar a chave API v\u00e1 a https://developer.airly.eu/register" } } } diff --git a/homeassistant/components/airnow/translations/pt.json b/homeassistant/components/airnow/translations/pt.json index f846047c307..dd714aa6dad 100644 --- a/homeassistant/components/airnow/translations/pt.json +++ b/homeassistant/components/airnow/translations/pt.json @@ -14,7 +14,8 @@ "api_key": "Chave da API", "latitude": "Latitude", "longitude": "Longitude" - } + }, + "description": "Para gerar a chave API v\u00e1 a https://docs.airnowapi.org/account/request/" } } } diff --git a/homeassistant/components/airthings/translations/pt.json b/homeassistant/components/airthings/translations/pt.json index 3b5850222d9..ef1f838f139 100644 --- a/homeassistant/components/airthings/translations/pt.json +++ b/homeassistant/components/airthings/translations/pt.json @@ -1,7 +1,21 @@ { "config": { + "abort": { + "already_configured": "Conta j\u00e1 configurada" + }, "error": { - "cannot_connect": "Falha na liga\u00e7\u00e3o" + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "description": "Fa\u00e7a login em {url} para encontrar as suas credenciais", + "id": "ID", + "secret": "Segredo" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/airtouch4/climate.py b/homeassistant/components/airtouch4/climate.py index dcc107453d5..598b6ecd6e3 100644 --- a/homeassistant/components/airtouch4/climate.py +++ b/homeassistant/components/airtouch4/climate.py @@ -4,14 +4,14 @@ from __future__ import annotations import logging from typing import Any -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( FAN_AUTO, FAN_DIFFUSE, FAN_FOCUS, FAN_HIGH, FAN_LOW, FAN_MEDIUM, + ClimateEntity, ClimateEntityFeature, HVACMode, ) diff --git a/homeassistant/components/airtouch4/translations/pt.json b/homeassistant/components/airtouch4/translations/pt.json index 4e8578a0a28..18ca3271c81 100644 --- a/homeassistant/components/airtouch4/translations/pt.json +++ b/homeassistant/components/airtouch4/translations/pt.json @@ -1,5 +1,11 @@ { "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha de liga\u00e7\u00e3o" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/airvisual/translations/es.json b/homeassistant/components/airvisual/translations/es.json index acffe47f3ca..33802db6415 100644 --- a/homeassistant/components/airvisual/translations/es.json +++ b/homeassistant/components/airvisual/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "La ubicaci\u00f3n ya est\u00e1 configurada o el Nodo/Pro ID ya est\u00e1 registrado.", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "error": { "cannot_connect": "No se pudo conectar", diff --git a/homeassistant/components/airvisual/translations/sensor.ja.json b/homeassistant/components/airvisual/translations/sensor.ja.json index 91bd016b0ac..718fa0ed158 100644 --- a/homeassistant/components/airvisual/translations/sensor.ja.json +++ b/homeassistant/components/airvisual/translations/sensor.ja.json @@ -11,7 +11,7 @@ "airvisual__pollutant_level": { "good": "\u826f\u597d", "hazardous": "\u5371\u967a", - "moderate": "\u9069\u5ea6", + "moderate": "\u4e2d\u7a0b\u5ea6", "unhealthy": "\u4e0d\u5065\u5eb7", "unhealthy_sensitive": "\u654f\u611f\u306a\u30b0\u30eb\u30fc\u30d7\u306b\u3068\u3063\u3066\u306f\u4e0d\u5065\u5eb7", "very_unhealthy": "\u3068\u3066\u3082\u4e0d\u5065\u5eb7" diff --git a/homeassistant/components/airzone/climate.py b/homeassistant/components/airzone/climate.py index ce67142547f..fa64efa355b 100644 --- a/homeassistant/components/airzone/climate.py +++ b/homeassistant/components/airzone/climate.py @@ -27,8 +27,8 @@ from aioairzone.const import ( ) from aioairzone.exceptions import AirzoneError -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( + ClimateEntity, ClimateEntityFeature, HVACAction, HVACMode, diff --git a/homeassistant/components/airzone/translations/pt.json b/homeassistant/components/airzone/translations/pt.json index f681da4210f..fa5aa3de317 100644 --- a/homeassistant/components/airzone/translations/pt.json +++ b/homeassistant/components/airzone/translations/pt.json @@ -1,5 +1,11 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/aladdin_connect/__init__.py b/homeassistant/components/aladdin_connect/__init__.py index 40dd0dd981a..e66efc1b0ab 100644 --- a/homeassistant/components/aladdin_connect/__init__.py +++ b/homeassistant/components/aladdin_connect/__init__.py @@ -4,6 +4,7 @@ import logging from typing import Final from AIOAladdinConnect import AladdinConnectClient +from AIOAladdinConnect.session_manager import InvalidPasswordError from aiohttp import ClientConnectionError from homeassistant.config_entries import ConfigEntry @@ -27,10 +28,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: username, password, async_get_clientsession(hass), CLIENT_ID ) try: - if not await acc.login(): - raise ConfigEntryAuthFailed("Incorrect Password") + await acc.login() except (ClientConnectionError, asyncio.TimeoutError) as ex: raise ConfigEntryNotReady("Can not connect to host") from ex + except InvalidPasswordError as ex: + raise ConfigEntryAuthFailed("Incorrect Password") from ex hass.data.setdefault(DOMAIN, {})[entry.entry_id] = acc await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) diff --git a/homeassistant/components/aladdin_connect/config_flow.py b/homeassistant/components/aladdin_connect/config_flow.py index 4f03d7cdb3b..1bfa9757907 100644 --- a/homeassistant/components/aladdin_connect/config_flow.py +++ b/homeassistant/components/aladdin_connect/config_flow.py @@ -7,7 +7,7 @@ import logging from typing import Any from AIOAladdinConnect import AladdinConnectClient -from aiohttp import ClientError +from AIOAladdinConnect.session_manager import InvalidPasswordError from aiohttp.client_exceptions import ClientConnectionError import voluptuous as vol @@ -43,9 +43,13 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> None: async_get_clientsession(hass), CLIENT_ID, ) - login = await acc.login() - if not login: - raise InvalidAuth + try: + await acc.login() + except (ClientConnectionError, asyncio.TimeoutError) as ex: + raise ex + + except InvalidPasswordError as ex: + raise InvalidAuth from ex class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @@ -80,7 +84,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): except InvalidAuth: errors["base"] = "invalid_auth" - except (ClientConnectionError, asyncio.TimeoutError, ClientError): + except (ClientConnectionError, asyncio.TimeoutError): errors["base"] = "cannot_connect" else: @@ -117,7 +121,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): except InvalidAuth: errors["base"] = "invalid_auth" - except (ClientConnectionError, asyncio.TimeoutError, ClientError): + except (ClientConnectionError, asyncio.TimeoutError): errors["base"] = "cannot_connect" else: diff --git a/homeassistant/components/aladdin_connect/manifest.json b/homeassistant/components/aladdin_connect/manifest.json index 3a3efa0f4a2..50ab6af6f85 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.44"], + "requirements": ["AIOAladdinConnect==0.1.46"], "codeowners": ["@mkmer"], "iot_class": "cloud_polling", "loggers": ["aladdin_connect"], diff --git a/homeassistant/components/aladdin_connect/translations/cs.json b/homeassistant/components/aladdin_connect/translations/cs.json index a3144ba2f55..007d1eb6704 100644 --- a/homeassistant/components/aladdin_connect/translations/cs.json +++ b/homeassistant/components/aladdin_connect/translations/cs.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" + }, "error": { "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed" diff --git a/homeassistant/components/aladdin_connect/translations/es.json b/homeassistant/components/aladdin_connect/translations/es.json index 5621a1c69e9..e412d52efef 100644 --- a/homeassistant/components/aladdin_connect/translations/es.json +++ b/homeassistant/components/aladdin_connect/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "error": { "cannot_connect": "No se pudo conectar", diff --git a/homeassistant/components/aladdin_connect/translations/pt.json b/homeassistant/components/aladdin_connect/translations/pt.json index 6c09ed1c852..5c109d9bc33 100644 --- a/homeassistant/components/aladdin_connect/translations/pt.json +++ b/homeassistant/components/aladdin_connect/translations/pt.json @@ -1,12 +1,21 @@ { "config": { "abort": { + "already_configured": "Dispositivo j\u00e1 configurado", "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida" }, "error": { + "cannot_connect": "Falha de liga\u00e7\u00e3o", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" }, "step": { + "reauth_confirm": { + "data": { + "password": "Palavra-passe" + }, + "description": "A integra\u00e7\u00e3o Aladdin Connect precisa de re-autenticar a sua conta", + "title": "Re-autenticar integra\u00e7\u00e3o" + }, "user": { "data": { "username": "Nome de Utilizador" diff --git a/homeassistant/components/alarm_control_panel/__init__.py b/homeassistant/components/alarm_control_panel/__init__.py index d89dbd280ef..4d74a39d977 100644 --- a/homeassistant/components/alarm_control_panel/__init__.py +++ b/homeassistant/components/alarm_control_panel/__init__.py @@ -55,10 +55,12 @@ ALARM_SERVICE_SCHEMA: Final = make_entity_service_schema( PLATFORM_SCHEMA: Final = cv.PLATFORM_SCHEMA PLATFORM_SCHEMA_BASE: Final = cv.PLATFORM_SCHEMA_BASE +# mypy: disallow-any-generics + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Track states and offer events for sensors.""" - component = hass.data[DOMAIN] = EntityComponent( + component = hass.data[DOMAIN] = EntityComponent[AlarmControlPanelEntity]( _LOGGER, DOMAIN, hass, SCAN_INTERVAL ) @@ -109,13 +111,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry.""" - component: EntityComponent = hass.data[DOMAIN] + component: EntityComponent[AlarmControlPanelEntity] = hass.data[DOMAIN] return await component.async_setup_entry(entry) async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - component: EntityComponent = hass.data[DOMAIN] + component: EntityComponent[AlarmControlPanelEntity] = hass.data[DOMAIN] return await component.async_unload_entry(entry) diff --git a/homeassistant/components/alarm_control_panel/manifest.json b/homeassistant/components/alarm_control_panel/manifest.json index 461094e8ce6..426e1e15afb 100644 --- a/homeassistant/components/alarm_control_panel/manifest.json +++ b/homeassistant/components/alarm_control_panel/manifest.json @@ -3,5 +3,6 @@ "name": "Alarm Control Panel", "documentation": "https://www.home-assistant.io/integrations/alarm_control_panel", "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "entity" } diff --git a/homeassistant/components/alarm_control_panel/translations/ja.json b/homeassistant/components/alarm_control_panel/translations/ja.json index 875d24fd3fe..dbcf46d76da 100644 --- a/homeassistant/components/alarm_control_panel/translations/ja.json +++ b/homeassistant/components/alarm_control_panel/translations/ja.json @@ -4,7 +4,7 @@ "arm_away": "\u8b66\u6212 {entity_name} \u96e2\u5e2d(away)", "arm_home": "\u8b66\u6212 {entity_name} \u5728\u5b85", "arm_night": "\u8b66\u6212 {entity_name} \u591c", - "arm_vacation": "\u8b66\u6212 {entity_name} \u4f11\u6687", + "arm_vacation": "\u30a2\u30fc\u30e0{entity_name}\u4f11\u6687", "disarm": "\u89e3\u9664 {entity_name}", "trigger": "\u30c8\u30ea\u30ac\u30fc {entity_name}" }, @@ -12,7 +12,7 @@ "is_armed_away": "{entity_name} \u306f\u8b66\u6212 \u96e2\u5e2d(away)", "is_armed_home": "{entity_name} \u306f\u8b66\u6212 \u5728\u5b85", "is_armed_night": "{entity_name} \u306f\u8b66\u6212 \u591c", - "is_armed_vacation": "{entity_name} \u306f\u8b66\u6212 \u4f11\u6687", + "is_armed_vacation": "{entity_name}\u306f\u6b66\u88c5\u4f11\u6687\u4e2d\u3067\u3059", "is_disarmed": "{entity_name} \u306f\u89e3\u9664", "is_triggered": "{entity_name} \u304c\u30c8\u30ea\u30ac\u30fc\u3055\u308c\u307e\u3059" }, @@ -20,7 +20,7 @@ "armed_away": "{entity_name} \u8b66\u6212 \u96e2\u5e2d(away)", "armed_home": "{entity_name} \u8b66\u6212 \u5728\u5b85", "armed_night": "{entity_name} \u8b66\u6212 \u591c", - "armed_vacation": "{entity_name} \u8b66\u6212 \u4f11\u6687", + "armed_vacation": "{entity_name}\u6b66\u88c5\u4f11\u6687", "disarmed": "{entity_name} \u89e3\u9664", "triggered": "{entity_name} \u304c\u30c8\u30ea\u30ac\u30fc\u3055\u308c\u307e\u3057\u305f" } @@ -32,7 +32,7 @@ "armed_custom_bypass": "\u8b66\u6212 \u30ab\u30b9\u30bf\u30e0 \u30d0\u30a4\u30d1\u30b9", "armed_home": "\u8b66\u6212 \u5728\u5b85", "armed_night": "\u8b66\u6212 \u591c", - "armed_vacation": "\u8b66\u6212 \u4f11\u6687", + "armed_vacation": "\u6b66\u88c5\u4f11\u6687", "arming": "\u8b66\u6212\u4e2d", "disarmed": "\u89e3\u9664", "disarming": "\u89e3\u9664", diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index dfac5c89ffd..15870c7bbfa 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -5,12 +5,14 @@ import logging from homeassistant.components import ( button, + climate, cover, fan, image_processing, input_button, input_number, light, + media_player, timer, vacuum, ) @@ -18,8 +20,6 @@ from homeassistant.components.alarm_control_panel import ( AlarmControlPanelEntityFeature, CodeFormat, ) -import homeassistant.components.climate.const as climate -import homeassistant.components.media_player.const as media_player from homeassistant.const import ( ATTR_CODE_FORMAT, ATTR_SUPPORTED_FEATURES, diff --git a/homeassistant/components/alexa/const.py b/homeassistant/components/alexa/const.py index d51409a5a1c..d1061720718 100644 --- a/homeassistant/components/alexa/const.py +++ b/homeassistant/components/alexa/const.py @@ -1,7 +1,7 @@ """Constants for the Alexa integration.""" from collections import OrderedDict -from homeassistant.components.climate import const as climate +from homeassistant.components import climate from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT DOMAIN = "alexa" diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py index ac78dbeed5e..e002969952a 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -11,6 +11,7 @@ from homeassistant.components import ( binary_sensor, button, camera, + climate, cover, fan, group, @@ -28,7 +29,6 @@ from homeassistant.components import ( timer, vacuum, ) -from homeassistant.components.climate import const as climate from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_SUPPORTED_FEATURES, @@ -300,12 +300,6 @@ class AlexaEntity: """ raise NotImplementedError - def get_interface(self, capability) -> AlexaCapability: - """Return the given AlexaInterface. - - Raises _UnsupportedInterface. - """ - def interfaces(self) -> list[AlexaCapability]: """Return a list of supported interfaces. diff --git a/homeassistant/components/alexa/errors.py b/homeassistant/components/alexa/errors.py index 0ce00f1fe48..5f0de6f7467 100644 --- a/homeassistant/components/alexa/errors.py +++ b/homeassistant/components/alexa/errors.py @@ -8,10 +8,6 @@ from homeassistant.exceptions import HomeAssistantError from .const import API_TEMP_UNITS -class UnsupportedInterface(HomeAssistantError): - """This entity does not support the requested Smart Home API interface.""" - - class UnsupportedProperty(HomeAssistantError): """This entity does not support the requested Smart Home API property.""" diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py index ba3892a62f2..b4c842dd5b5 100644 --- a/homeassistant/components/alexa/handlers.py +++ b/homeassistant/components/alexa/handlers.py @@ -10,6 +10,7 @@ from homeassistant import core as ha from homeassistant.components import ( button, camera, + climate, cover, fan, group, @@ -20,7 +21,6 @@ from homeassistant.components import ( timer, vacuum, ) -from homeassistant.components.climate import const as climate from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_ENTITY_PICTURE, @@ -50,10 +50,9 @@ from homeassistant.const import ( TEMP_FAHRENHEIT, ) from homeassistant.helpers import network -import homeassistant.util.color as color_util +from homeassistant.util import color as color_util, dt as dt_util from homeassistant.util.decorator import Registry -import homeassistant.util.dt as dt_util -from homeassistant.util.temperature import convert as convert_temperature +from homeassistant.util.unit_conversion import TemperatureConverter from .config import AbstractConfig from .const import ( @@ -820,7 +819,9 @@ def temperature_from_object(hass, temp_obj, interval=False): # convert to Celsius if absolute temperature temp -= 273.15 - return convert_temperature(temp, from_unit, to_unit, interval) + if interval: + return TemperatureConverter.convert_interval(temp, from_unit, to_unit) + return TemperatureConverter.convert(temp, from_unit, to_unit) @HANDLERS.register(("Alexa.ThermostatController", "SetTargetTemperature")) diff --git a/homeassistant/components/alexa/logbook.py b/homeassistant/components/alexa/logbook.py index b72b884ec29..079fea99fdf 100644 --- a/homeassistant/components/alexa/logbook.py +++ b/homeassistant/components/alexa/logbook.py @@ -1,5 +1,5 @@ """Describe logbook events.""" -from homeassistant.components.logbook.const import ( +from homeassistant.components.logbook import ( LOGBOOK_ENTRY_ENTITY_ID, LOGBOOK_ENTRY_MESSAGE, LOGBOOK_ENTRY_NAME, diff --git a/homeassistant/components/ambee/__init__.py b/homeassistant/components/ambee/__init__.py deleted file mode 100644 index 547b8720fef..00000000000 --- a/homeassistant/components/ambee/__init__.py +++ /dev/null @@ -1,92 +0,0 @@ -"""Support for Ambee.""" -from __future__ import annotations - -from ambee import AirQuality, Ambee, AmbeeAuthenticationError, Pollen - -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, Platform -from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryAuthFailed -from homeassistant.helpers.issue_registry import ( - IssueSeverity, - async_create_issue, - async_delete_issue, -) -from homeassistant.helpers.typing import ConfigType -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator - -from .const import DOMAIN, LOGGER, SCAN_INTERVAL, SERVICE_AIR_QUALITY, SERVICE_POLLEN - -PLATFORMS = [Platform.SENSOR] - - -async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up the Ambee integration.""" - async_create_issue( - hass, - DOMAIN, - "pending_removal", - breaks_in_ha_version="2022.10.0", - is_fixable=False, - severity=IssueSeverity.WARNING, - translation_key="pending_removal", - ) - return True - - -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Set up Ambee from a config entry.""" - hass.data.setdefault(DOMAIN, {}).setdefault(entry.entry_id, {}) - - client = Ambee( - api_key=entry.data[CONF_API_KEY], - latitude=entry.data[CONF_LATITUDE], - longitude=entry.data[CONF_LONGITUDE], - ) - - async def update_air_quality() -> AirQuality: - """Update method for updating Ambee Air Quality data.""" - try: - return await client.air_quality() - except AmbeeAuthenticationError as err: - raise ConfigEntryAuthFailed from err - - air_quality: DataUpdateCoordinator[AirQuality] = DataUpdateCoordinator( - hass, - LOGGER, - name=f"{DOMAIN}_{SERVICE_AIR_QUALITY}", - update_interval=SCAN_INTERVAL, - update_method=update_air_quality, - ) - await air_quality.async_config_entry_first_refresh() - hass.data[DOMAIN][entry.entry_id][SERVICE_AIR_QUALITY] = air_quality - - async def update_pollen() -> Pollen: - """Update method for updating Ambee Pollen data.""" - try: - return await client.pollen() - except AmbeeAuthenticationError as err: - raise ConfigEntryAuthFailed from err - - pollen: DataUpdateCoordinator[Pollen] = DataUpdateCoordinator( - hass, - LOGGER, - name=f"{DOMAIN}_{SERVICE_POLLEN}", - update_interval=SCAN_INTERVAL, - update_method=update_pollen, - ) - await pollen.async_config_entry_first_refresh() - hass.data[DOMAIN][entry.entry_id][SERVICE_POLLEN] = pollen - - await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) - return True - - -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Unload Ambee config entry.""" - unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - if unload_ok: - del hass.data[DOMAIN][entry.entry_id] - if not hass.data[DOMAIN]: - async_delete_issue(hass, DOMAIN, "pending_removal") - return unload_ok diff --git a/homeassistant/components/ambee/config_flow.py b/homeassistant/components/ambee/config_flow.py deleted file mode 100644 index 7bfc1fa11af..00000000000 --- a/homeassistant/components/ambee/config_flow.py +++ /dev/null @@ -1,116 +0,0 @@ -"""Config flow to configure the Ambee integration.""" -from __future__ import annotations - -from collections.abc import Mapping -from typing import Any - -from ambee import Ambee, AmbeeAuthenticationError, AmbeeError -import voluptuous as vol - -from homeassistant.config_entries import ConfigEntry, ConfigFlow -from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME -from homeassistant.data_entry_flow import FlowResult -from homeassistant.helpers.aiohttp_client import async_get_clientsession -import homeassistant.helpers.config_validation as cv - -from .const import DOMAIN - - -class AmbeeFlowHandler(ConfigFlow, domain=DOMAIN): - """Config flow for Ambee.""" - - VERSION = 1 - - entry: ConfigEntry | None = None - - async def async_step_user( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: - """Handle a flow initialized by the user.""" - errors = {} - - if user_input is not None: - session = async_get_clientsession(self.hass) - try: - client = Ambee( - api_key=user_input[CONF_API_KEY], - latitude=user_input[CONF_LATITUDE], - longitude=user_input[CONF_LONGITUDE], - session=session, - ) - await client.air_quality() - except AmbeeAuthenticationError: - errors["base"] = "invalid_api_key" - except AmbeeError: - errors["base"] = "cannot_connect" - else: - return self.async_create_entry( - title=user_input[CONF_NAME], - data={ - CONF_API_KEY: user_input[CONF_API_KEY], - CONF_LATITUDE: user_input[CONF_LATITUDE], - CONF_LONGITUDE: user_input[CONF_LONGITUDE], - }, - ) - - return self.async_show_form( - step_id="user", - data_schema=vol.Schema( - { - vol.Required(CONF_API_KEY): str, - vol.Optional( - CONF_NAME, default=self.hass.config.location_name - ): str, - vol.Optional( - CONF_LATITUDE, default=self.hass.config.latitude - ): cv.latitude, - vol.Optional( - CONF_LONGITUDE, default=self.hass.config.longitude - ): cv.longitude, - } - ), - errors=errors, - ) - - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: - """Handle initiation of re-authentication with Ambee.""" - self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) - return await self.async_step_reauth_confirm() - - async def async_step_reauth_confirm( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: - """Handle re-authentication with Ambee.""" - errors = {} - if user_input is not None and self.entry: - session = async_get_clientsession(self.hass) - client = Ambee( - api_key=user_input[CONF_API_KEY], - latitude=self.entry.data[CONF_LATITUDE], - longitude=self.entry.data[CONF_LONGITUDE], - session=session, - ) - try: - await client.air_quality() - except AmbeeAuthenticationError: - errors["base"] = "invalid_api_key" - except AmbeeError: - errors["base"] = "cannot_connect" - else: - self.hass.config_entries.async_update_entry( - self.entry, - data={ - **self.entry.data, - CONF_API_KEY: user_input[CONF_API_KEY], - }, - ) - self.hass.async_create_task( - self.hass.config_entries.async_reload(self.entry.entry_id) - ) - return self.async_abort(reason="reauth_successful") - - return self.async_show_form( - step_id="reauth_confirm", - data_schema=vol.Schema({vol.Required(CONF_API_KEY): str}), - errors=errors, - ) diff --git a/homeassistant/components/ambee/const.py b/homeassistant/components/ambee/const.py deleted file mode 100644 index 83abb841629..00000000000 --- a/homeassistant/components/ambee/const.py +++ /dev/null @@ -1,232 +0,0 @@ -"""Constants for the Ambee integration.""" -from __future__ import annotations - -from datetime import timedelta -import logging -from typing import Final - -from homeassistant.components.sensor import ( - SensorDeviceClass, - SensorEntityDescription, - SensorStateClass, -) -from homeassistant.const import ( - CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - CONCENTRATION_PARTS_PER_BILLION, - CONCENTRATION_PARTS_PER_CUBIC_METER, - CONCENTRATION_PARTS_PER_MILLION, -) - -DOMAIN: Final = "ambee" -LOGGER = logging.getLogger(__package__) -SCAN_INTERVAL = timedelta(hours=1) - -DEVICE_CLASS_AMBEE_RISK: Final = "ambee__risk" - -SERVICE_AIR_QUALITY: Final = "air_quality" -SERVICE_POLLEN: Final = "pollen" - -SERVICES: dict[str, str] = { - SERVICE_AIR_QUALITY: "Air quality", - SERVICE_POLLEN: "Pollen", -} - -SENSORS: dict[str, list[SensorEntityDescription]] = { - SERVICE_AIR_QUALITY: [ - SensorEntityDescription( - key="particulate_matter_2_5", - name="Particulate matter < 2.5 μm", - native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key="particulate_matter_10", - name="Particulate matter < 10 μm", - native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key="sulphur_dioxide", - name="Sulphur dioxide (SO2)", - native_unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key="nitrogen_dioxide", - name="Nitrogen dioxide (NO2)", - native_unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key="ozone", - name="Ozone", - native_unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key="carbon_monoxide", - name="Carbon monoxide (CO)", - device_class=SensorDeviceClass.CO, - native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key="air_quality_index", - name="Air quality index (AQI)", - state_class=SensorStateClass.MEASUREMENT, - ), - ], - SERVICE_POLLEN: [ - SensorEntityDescription( - key="grass", - name="Grass", - icon="mdi:grass", - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, - ), - SensorEntityDescription( - key="tree", - name="Tree", - icon="mdi:tree", - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, - ), - SensorEntityDescription( - key="weed", - name="Weed", - icon="mdi:sprout", - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, - ), - SensorEntityDescription( - key="grass_risk", - name="Grass risk", - icon="mdi:grass", - device_class=DEVICE_CLASS_AMBEE_RISK, - ), - SensorEntityDescription( - key="tree_risk", - name="Tree risk", - icon="mdi:tree", - device_class=DEVICE_CLASS_AMBEE_RISK, - ), - SensorEntityDescription( - key="weed_risk", - name="Weed risk", - icon="mdi:sprout", - device_class=DEVICE_CLASS_AMBEE_RISK, - ), - SensorEntityDescription( - key="grass_poaceae", - name="Poaceae grass", - icon="mdi:grass", - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, - entity_registry_enabled_default=False, - ), - SensorEntityDescription( - key="tree_alder", - name="Alder tree", - icon="mdi:tree", - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, - entity_registry_enabled_default=False, - ), - SensorEntityDescription( - key="tree_birch", - name="Birch tree", - icon="mdi:tree", - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, - entity_registry_enabled_default=False, - ), - SensorEntityDescription( - key="tree_cypress", - name="Cypress tree", - icon="mdi:tree", - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, - entity_registry_enabled_default=False, - ), - SensorEntityDescription( - key="tree_elm", - name="Elm tree", - icon="mdi:tree", - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, - entity_registry_enabled_default=False, - ), - SensorEntityDescription( - key="tree_hazel", - name="Hazel tree", - icon="mdi:tree", - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, - entity_registry_enabled_default=False, - ), - SensorEntityDescription( - key="tree_oak", - name="Oak tree", - icon="mdi:tree", - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, - entity_registry_enabled_default=False, - ), - SensorEntityDescription( - key="tree_pine", - name="Pine tree", - icon="mdi:tree", - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, - entity_registry_enabled_default=False, - ), - SensorEntityDescription( - key="tree_plane", - name="Plane tree", - icon="mdi:tree", - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, - entity_registry_enabled_default=False, - ), - SensorEntityDescription( - key="tree_poplar", - name="Poplar tree", - icon="mdi:tree", - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, - entity_registry_enabled_default=False, - ), - SensorEntityDescription( - key="weed_chenopod", - name="Chenopod weed", - icon="mdi:sprout", - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, - entity_registry_enabled_default=False, - ), - SensorEntityDescription( - key="weed_mugwort", - name="Mugwort weed", - icon="mdi:sprout", - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, - entity_registry_enabled_default=False, - ), - SensorEntityDescription( - key="weed_nettle", - name="Nettle weed", - icon="mdi:sprout", - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, - entity_registry_enabled_default=False, - ), - SensorEntityDescription( - key="weed_ragweed", - name="Ragweed weed", - icon="mdi:sprout", - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, - entity_registry_enabled_default=False, - ), - ], -} diff --git a/homeassistant/components/ambee/manifest.json b/homeassistant/components/ambee/manifest.json deleted file mode 100644 index 3226e9de3a3..00000000000 --- a/homeassistant/components/ambee/manifest.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "domain": "ambee", - "name": "Ambee", - "config_flow": true, - "documentation": "https://www.home-assistant.io/integrations/ambee", - "requirements": ["ambee==0.4.0"], - "codeowners": ["@frenck"], - "quality_scale": "platinum", - "iot_class": "cloud_polling" -} diff --git a/homeassistant/components/ambee/sensor.py b/homeassistant/components/ambee/sensor.py deleted file mode 100644 index 8fb6c9f2a61..00000000000 --- a/homeassistant/components/ambee/sensor.py +++ /dev/null @@ -1,77 +0,0 @@ -"""Support for Ambee sensors.""" -from __future__ import annotations - -from homeassistant.components.sensor import ( - DOMAIN as SENSOR_DOMAIN, - SensorEntity, - SensorEntityDescription, -) -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 homeassistant.helpers.typing import StateType -from homeassistant.helpers.update_coordinator import ( - CoordinatorEntity, - DataUpdateCoordinator, -) - -from .const import DOMAIN, SENSORS, SERVICES - - -async def async_setup_entry( - hass: HomeAssistant, - entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up Ambee sensors based on a config entry.""" - async_add_entities( - AmbeeSensorEntity( - coordinator=hass.data[DOMAIN][entry.entry_id][service_key], - entry_id=entry.entry_id, - description=description, - service_key=service_key, - service=SERVICES[service_key], - ) - for service_key, service_sensors in SENSORS.items() - for description in service_sensors - ) - - -class AmbeeSensorEntity(CoordinatorEntity, SensorEntity): - """Defines an Ambee sensor.""" - - _attr_has_entity_name = True - - def __init__( - self, - *, - coordinator: DataUpdateCoordinator, - entry_id: str, - description: SensorEntityDescription, - service_key: str, - service: str, - ) -> None: - """Initialize Ambee sensor.""" - super().__init__(coordinator=coordinator) - self._service_key = service_key - - self.entity_id = f"{SENSOR_DOMAIN}.{service_key}_{description.key}" - self.entity_description = description - self._attr_unique_id = f"{entry_id}_{service_key}_{description.key}" - - self._attr_device_info = DeviceInfo( - entry_type=DeviceEntryType.SERVICE, - identifiers={(DOMAIN, f"{entry_id}_{service_key}")}, - manufacturer="Ambee", - name=service, - ) - - @property - def native_value(self) -> StateType: - """Return the state of the sensor.""" - value = getattr(self.coordinator.data, self.entity_description.key) - if isinstance(value, str): - return value.lower() - return value # type: ignore[no-any-return] diff --git a/homeassistant/components/ambee/strings.json b/homeassistant/components/ambee/strings.json deleted file mode 100644 index 7d0e75877c9..00000000000 --- a/homeassistant/components/ambee/strings.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "config": { - "step": { - "user": { - "description": "Set up Ambee to integrate with Home Assistant.", - "data": { - "api_key": "[%key:common::config_flow::data::api_key%]", - "latitude": "[%key:common::config_flow::data::latitude%]", - "longitude": "[%key:common::config_flow::data::longitude%]", - "name": "[%key:common::config_flow::data::name%]" - } - }, - "reauth_confirm": { - "data": { - "description": "Re-authenticate with your Ambee account.", - "api_key": "[%key:common::config_flow::data::api_key%]" - } - } - }, - "error": { - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", - "invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]" - }, - "abort": { - "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" - } - }, - "issues": { - "pending_removal": { - "title": "The Ambee integration is being removed", - "description": "The Ambee integration is pending removal from Home Assistant and will no longer be available as of Home Assistant 2022.10.\n\nThe integration is being removed, because Ambee removed their free (limited) accounts and doesn't provide a way for regular users to sign up for a paid plan anymore.\n\nRemove the Ambee integration entry from your instance to fix this issue." - } - } -} diff --git a/homeassistant/components/ambee/strings.sensor.json b/homeassistant/components/ambee/strings.sensor.json deleted file mode 100644 index 83eb3b3fd73..00000000000 --- a/homeassistant/components/ambee/strings.sensor.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "state": { - "ambee__risk": { - "low": "Low", - "moderate": "Moderate", - "high": "High", - "very high": "Very High" - } - } -} diff --git a/homeassistant/components/ambee/translations/bg.json b/homeassistant/components/ambee/translations/bg.json deleted file mode 100644 index c72dc5227ca..00000000000 --- a/homeassistant/components/ambee/translations/bg.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "config": { - "abort": { - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" - }, - "error": { - "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", - "invalid_api_key": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d API \u043a\u043b\u044e\u0447" - }, - "step": { - "reauth_confirm": { - "data": { - "api_key": "API \u043a\u043b\u044e\u0447" - } - }, - "user": { - "data": { - "api_key": "API \u043a\u043b\u044e\u0447", - "latitude": "\u0413\u0435\u043e\u0433\u0440\u0430\u0444\u0441\u043a\u0430 \u0448\u0438\u0440\u0438\u043d\u0430", - "longitude": "\u0413\u0435\u043e\u0433\u0440\u0430\u0444\u0441\u043a\u0430 \u0434\u044a\u043b\u0436\u0438\u043d\u0430", - "name": "\u0418\u043c\u0435" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/ca.json b/homeassistant/components/ambee/translations/ca.json deleted file mode 100644 index bb4d49642b5..00000000000 --- a/homeassistant/components/ambee/translations/ca.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "config": { - "abort": { - "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" - }, - "error": { - "cannot_connect": "Ha fallat la connexi\u00f3", - "invalid_api_key": "Clau API inv\u00e0lida" - }, - "step": { - "reauth_confirm": { - "data": { - "api_key": "Clau API", - "description": "Torna a autenticar-te amb el compte d'Ambee." - } - }, - "user": { - "data": { - "api_key": "Clau API", - "latitude": "Latitud", - "longitude": "Longitud", - "name": "Nom" - }, - "description": "Configura la integraci\u00f3 d'Ambee amb Home Assistant." - } - } - }, - "issues": { - "pending_removal": { - "description": "La integraci\u00f3 d'Ambee s'eliminar\u00e0 de Home Assistant i deixar\u00e0 d'estar disponible a la versi\u00f3 de Home Assistant 2022.10.\n\nLa integraci\u00f3 s'eliminar\u00e0 perqu\u00e8 Ambee ha eliminat els seus comptes gratu\u00efts (limitats) i no ha donat cap manera per als usuaris normals de registrar-se a un pla de pagament.\n\nElimina la integraci\u00f3 d'Ambee del Home Assistant per solucionar aquest problema.", - "title": "La integraci\u00f3 Ambee est\u00e0 sent eliminada" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/cs.json b/homeassistant/components/ambee/translations/cs.json deleted file mode 100644 index 6459ddb3ba0..00000000000 --- a/homeassistant/components/ambee/translations/cs.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "config": { - "abort": { - "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" - }, - "step": { - "user": { - "data": { - "longitude": "Zem\u011bpisn\u00e1 d\u00e9lka", - "name": "Jm\u00e9no" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/de.json b/homeassistant/components/ambee/translations/de.json deleted file mode 100644 index 8055ef5210f..00000000000 --- a/homeassistant/components/ambee/translations/de.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "config": { - "abort": { - "reauth_successful": "Die erneute Authentifizierung war erfolgreich" - }, - "error": { - "cannot_connect": "Verbindung fehlgeschlagen", - "invalid_api_key": "Ung\u00fcltiger API-Schl\u00fcssel" - }, - "step": { - "reauth_confirm": { - "data": { - "api_key": "API-Schl\u00fcssel", - "description": "Authentifiziere dich erneut mit deinem Ambee-Konto." - } - }, - "user": { - "data": { - "api_key": "API-Schl\u00fcssel", - "latitude": "Breitengrad", - "longitude": "L\u00e4ngengrad", - "name": "Name" - }, - "description": "Richte Ambee f\u00fcr die Integration mit Home Assistant ein." - } - } - }, - "issues": { - "pending_removal": { - "description": "Die Ambee-Integration ist dabei, aus Home Assistant entfernt zu werden und wird ab Home Assistant 2022.10 nicht mehr verf\u00fcgbar sein.\n\nDie Integration wird entfernt, weil Ambee seine kostenlosen (begrenzten) Konten entfernt hat und keine M\u00f6glichkeit mehr f\u00fcr regul\u00e4re Nutzer bietet, sich f\u00fcr einen kostenpflichtigen Plan anzumelden.\n\nEntferne den Ambee-Integrationseintrag aus deiner Instanz, um dieses Problem zu beheben.", - "title": "Die Ambee-Integration wird entfernt" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/el.json b/homeassistant/components/ambee/translations/el.json deleted file mode 100644 index 99198a39817..00000000000 --- a/homeassistant/components/ambee/translations/el.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "config": { - "abort": { - "reauth_successful": "\u039f \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2" - }, - "error": { - "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", - "invalid_api_key": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API" - }, - "step": { - "reauth_confirm": { - "data": { - "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API", - "description": "\u0395\u03c0\u03b1\u03bd\u03b1\u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf\u03bd \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc \u03c3\u03b1\u03c2 Ambee." - } - }, - "user": { - "data": { - "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API", - "latitude": "\u0393\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03b9\u03ba\u03cc \u03c0\u03bb\u03ac\u03c4\u03bf\u03c2", - "longitude": "\u0393\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03b9\u03ba\u03cc \u03bc\u03ae\u03ba\u03bf\u03c2", - "name": "\u038c\u03bd\u03bf\u03bc\u03b1" - }, - "description": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03c4\u03bf Ambee \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b5\u03bd\u03c3\u03c9\u03bc\u03b1\u03c4\u03c9\u03b8\u03b5\u03af \u03bc\u03b5 \u03c4\u03bf Home Assistant." - } - } - }, - "issues": { - "pending_removal": { - "description": "\u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 Ambee \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 \u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9, \u03b5\u03c0\u03b5\u03b9\u03b4\u03ae \u03b7 Ambee \u03b1\u03c6\u03b1\u03af\u03c1\u03b5\u03c3\u03b5 \u03c4\u03bf\u03c5\u03c2 \u03b4\u03c9\u03c1\u03b5\u03ac\u03bd (\u03c0\u03b5\u03c1\u03b9\u03bf\u03c1\u03b9\u03c3\u03bc\u03ad\u03bd\u03bf\u03c5\u03c2) \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03bf\u03cd\u03c2 \u03c4\u03bf\u03c5\u03c2 \u03ba\u03b1\u03b9 \u03b4\u03b5\u03bd \u03c0\u03b1\u03c1\u03ad\u03c7\u03b5\u03b9 \u03c0\u03bb\u03ad\u03bf\u03bd \u03c4\u03c1\u03cc\u03c0\u03bf \u03c3\u03c4\u03bf\u03c5\u03c2 \u03c4\u03b1\u03ba\u03c4\u03b9\u03ba\u03bf\u03cd\u03c2 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b5\u03c2 \u03bd\u03b1 \u03b5\u03b3\u03b3\u03c1\u03b1\u03c6\u03bf\u03cd\u03bd \u03c3\u03b5 \u03ad\u03bd\u03b1 \u03c0\u03c1\u03cc\u03b3\u03c1\u03b1\u03bc\u03bc\u03b1 \u03b5\u03c0\u03af \u03c0\u03bb\u03b7\u03c1\u03c9\u03bc\u03ae. \n\n \u039a\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03ba\u03b1\u03c4\u03b1\u03c7\u03ce\u03c1\u03b7\u03c3\u03b7 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2 Ambee \u03b1\u03c0\u03cc \u03c4\u03b7\u03bd \u03c0\u03b1\u03c1\u03bf\u03c5\u03c3\u03af\u03b1 \u03c3\u03b1\u03c2 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03b6\u03ae\u03c4\u03b7\u03bc\u03b1.", - "title": "\u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 Ambee \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/en.json b/homeassistant/components/ambee/translations/en.json deleted file mode 100644 index 03f4c3241b6..00000000000 --- a/homeassistant/components/ambee/translations/en.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "config": { - "abort": { - "reauth_successful": "Re-authentication was successful" - }, - "error": { - "cannot_connect": "Failed to connect", - "invalid_api_key": "Invalid API key" - }, - "step": { - "reauth_confirm": { - "data": { - "api_key": "API Key", - "description": "Re-authenticate with your Ambee account." - } - }, - "user": { - "data": { - "api_key": "API Key", - "latitude": "Latitude", - "longitude": "Longitude", - "name": "Name" - }, - "description": "Set up Ambee to integrate with Home Assistant." - } - } - }, - "issues": { - "pending_removal": { - "description": "The Ambee integration is pending removal from Home Assistant and will no longer be available as of Home Assistant 2022.10.\n\nThe integration is being removed, because Ambee removed their free (limited) accounts and doesn't provide a way for regular users to sign up for a paid plan anymore.\n\nRemove the Ambee integration entry from your instance to fix this issue.", - "title": "The Ambee integration is being removed" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/es-419.json b/homeassistant/components/ambee/translations/es-419.json deleted file mode 100644 index de5ce971fa0..00000000000 --- a/homeassistant/components/ambee/translations/es-419.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "config": { - "step": { - "reauth_confirm": { - "data": { - "description": "Vuelva a autenticarse con su cuenta de Ambee." - } - }, - "user": { - "description": "Configure Ambee para que se integre con Home Assistant." - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/es.json b/homeassistant/components/ambee/translations/es.json deleted file mode 100644 index fde555ad801..00000000000 --- a/homeassistant/components/ambee/translations/es.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "config": { - "abort": { - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" - }, - "error": { - "cannot_connect": "No se pudo conectar", - "invalid_api_key": "Clave API no v\u00e1lida" - }, - "step": { - "reauth_confirm": { - "data": { - "api_key": "Clave API", - "description": "Vuelve a autenticarse con tu cuenta de Ambee." - } - }, - "user": { - "data": { - "api_key": "Clave API", - "latitude": "Latitud", - "longitude": "Longitud", - "name": "Nombre" - }, - "description": "Configura Ambee para que se integre con Home Assistant." - } - } - }, - "issues": { - "pending_removal": { - "description": "La integraci\u00f3n Ambee est\u00e1 pendiente de eliminaci\u00f3n de Home Assistant y ya no estar\u00e1 disponible a partir de Home Assistant 2022.10. \n\nSe va a eliminar la integraci\u00f3n porque Ambee elimin\u00f3 sus cuentas gratuitas (limitadas) y ya no proporciona una forma para que los usuarios regulares se registren en un plan pago. \n\nElimina la entrada de la integraci\u00f3n Ambee de tu instancia para solucionar este problema.", - "title": "Se va a eliminar la integraci\u00f3n Ambee" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/et.json b/homeassistant/components/ambee/translations/et.json deleted file mode 100644 index abb41497581..00000000000 --- a/homeassistant/components/ambee/translations/et.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "config": { - "abort": { - "reauth_successful": "Taastuvastamine \u00f5nnestus" - }, - "error": { - "cannot_connect": "\u00dchendumine nurjus", - "invalid_api_key": "Vale API v\u00f5ti" - }, - "step": { - "reauth_confirm": { - "data": { - "api_key": "API v\u00f5ti", - "description": "Taastuvasta Ambee konto" - } - }, - "user": { - "data": { - "api_key": "API v\u00f5ti", - "latitude": "Laiuskraad", - "longitude": "Pikkuskraad", - "name": "Nimi" - }, - "description": "Seadista Ambee sidumine Home Assistantiga." - } - } - }, - "issues": { - "pending_removal": { - "description": "Ambee integratsioon on Home Assistantist eemaldamisel ja ei ole enam saadaval alates Home Assistant 2022.10.\n\nIntegratsioon eemaldatakse, sest Ambee eemaldas oma tasuta (piiratud) kontod ja ei paku tavakasutajatele enam v\u00f5imalust tasulisele plaanile registreeruda.\n\nSelle probleemi lahendamiseks eemaldage Ambee integratsiooni kirje oma instantsist.", - "title": "Ambee integratsioon eemaldatakse" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/fr.json b/homeassistant/components/ambee/translations/fr.json deleted file mode 100644 index da3932962a6..00000000000 --- a/homeassistant/components/ambee/translations/fr.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "config": { - "abort": { - "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" - }, - "error": { - "cannot_connect": "\u00c9chec de connexion", - "invalid_api_key": "Cl\u00e9 d'API non valide" - }, - "step": { - "reauth_confirm": { - "data": { - "api_key": "Cl\u00e9 d'API", - "description": "R\u00e9-authentifiez-vous avec votre compte Ambee." - } - }, - "user": { - "data": { - "api_key": "Cl\u00e9 d'API", - "latitude": "Latitude", - "longitude": "Longitude", - "name": "Nom" - }, - "description": "Configurer Ambee pour l'int\u00e9grer \u00e0 Home Assistant." - } - } - }, - "issues": { - "pending_removal": { - "title": "L'int\u00e9gration Ambee est en cours de suppression" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/he.json b/homeassistant/components/ambee/translations/he.json deleted file mode 100644 index 7b7882cd4df..00000000000 --- a/homeassistant/components/ambee/translations/he.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "config": { - "abort": { - "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7" - }, - "error": { - "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", - "invalid_api_key": "\u05de\u05e4\u05ea\u05d7 API \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" - }, - "step": { - "reauth_confirm": { - "data": { - "api_key": "\u05de\u05e4\u05ea\u05d7 API" - } - }, - "user": { - "data": { - "api_key": "\u05de\u05e4\u05ea\u05d7 API", - "latitude": "\u05e7\u05d5 \u05e8\u05d5\u05d7\u05d1", - "longitude": "\u05e7\u05d5 \u05d0\u05d5\u05e8\u05da", - "name": "\u05e9\u05dd" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/hu.json b/homeassistant/components/ambee/translations/hu.json deleted file mode 100644 index 98e9fbdabea..00000000000 --- a/homeassistant/components/ambee/translations/hu.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "config": { - "abort": { - "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt." - }, - "error": { - "cannot_connect": "Sikertelen csatlakoz\u00e1s", - "invalid_api_key": "\u00c9rv\u00e9nytelen API kulcs" - }, - "step": { - "reauth_confirm": { - "data": { - "api_key": "API kulcs", - "description": "Hiteles\u00edtse mag\u00e1t \u00fajra az Ambee-fi\u00f3kj\u00e1val." - } - }, - "user": { - "data": { - "api_key": "API kulcs", - "latitude": "Sz\u00e9less\u00e9g", - "longitude": "Hossz\u00fas\u00e1g", - "name": "Elnevez\u00e9s" - }, - "description": "Integr\u00e1lja \u00f6ssze Ambeet Home Assistanttal." - } - } - }, - "issues": { - "pending_removal": { - "description": "Az Ambee integr\u00e1ci\u00f3 elt\u00e1vol\u00edt\u00e1sra v\u00e1r a Home Assistantb\u00f3l, \u00e9s a 2022.10-es Home Assistant-t\u00f3l m\u00e1r nem lesz el\u00e9rhet\u0151.\n\nAz integr\u00e1ci\u00f3 elt\u00e1vol\u00edt\u00e1sa az\u00e9rt t\u00f6rt\u00e9nik, mert az Ambee elt\u00e1vol\u00edtotta az ingyenes (korl\u00e1tozott) fi\u00f3kjait, \u00e9s a rendszeres felhaszn\u00e1l\u00f3k sz\u00e1m\u00e1ra m\u00e1r nem biztos\u00edt lehet\u0151s\u00e9get arra, hogy fizet\u0151s csomagra regisztr\u00e1ljanak.\n\nA hiba\u00fczenet elrejt\u00e9s\u00e9hez t\u00e1vol\u00edtsa el az Ambee integr\u00e1ci\u00f3s bejegyz\u00e9st a rendszerb\u0151l.", - "title": "Az Ambee integr\u00e1ci\u00f3 elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/id.json b/homeassistant/components/ambee/translations/id.json deleted file mode 100644 index 686e36fd17b..00000000000 --- a/homeassistant/components/ambee/translations/id.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "config": { - "abort": { - "reauth_successful": "Autentikasi ulang berhasil" - }, - "error": { - "cannot_connect": "Gagal terhubung", - "invalid_api_key": "Kunci API tidak valid" - }, - "step": { - "reauth_confirm": { - "data": { - "api_key": "Kunci API", - "description": "Autentikasi ulang dengan akun Ambee Anda." - } - }, - "user": { - "data": { - "api_key": "Kunci API", - "latitude": "Lintang", - "longitude": "Bujur", - "name": "Nama" - }, - "description": "Siapkan Ambee Anda untuk diintegrasikan dengan Home Assistant." - } - } - }, - "issues": { - "pending_removal": { - "description": "Integrasi Ambee sedang menunggu penghapusan dari Home Assistant dan tidak akan lagi tersedia pada Home Assistant 2022.10.\n\nIntegrasi ini dalam proses penghapusan, karena Ambee telah menghapus akun versi gratis (terbatas) mereka dan tidak menyediakan cara bagi pengguna biasa untuk mendaftar paket berbayar lagi.\n\nHapus entri integrasi Ambee dari instans Anda untuk memperbaiki masalah ini.", - "title": "Integrasi Ambee dalam proses penghapusan" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/it.json b/homeassistant/components/ambee/translations/it.json deleted file mode 100644 index f2054c8a6ff..00000000000 --- a/homeassistant/components/ambee/translations/it.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "config": { - "abort": { - "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" - }, - "error": { - "cannot_connect": "Impossibile connettersi", - "invalid_api_key": "Chiave API non valida" - }, - "step": { - "reauth_confirm": { - "data": { - "api_key": "Chiave API", - "description": "Autenticati nuovamente con il tuo account Ambee." - } - }, - "user": { - "data": { - "api_key": "Chiave API", - "latitude": "Latitudine", - "longitude": "Logitudine", - "name": "Nome" - }, - "description": "Configura Ambee per l'integrazione con Home Assistant." - } - } - }, - "issues": { - "pending_removal": { - "description": "L'integrazione Ambee \u00e8 in attesa di rimozione da Home Assistant e non sar\u00e0 pi\u00f9 disponibile a partire da Home Assistant 2022.10. \n\nL'integrazione \u00e8 stata rimossa, perch\u00e9 Ambee ha rimosso i loro account gratuiti (limitati) e non offre pi\u00f9 agli utenti regolari un modo per iscriversi a un piano a pagamento. \n\nRimuovi la voce di integrazione Ambee dalla tua istanza per risolvere questo problema.", - "title": "L'integrazione Ambee sar\u00e0 rimossa" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/ja.json b/homeassistant/components/ambee/translations/ja.json deleted file mode 100644 index 2d6bf3b2466..00000000000 --- a/homeassistant/components/ambee/translations/ja.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "config": { - "abort": { - "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" - }, - "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", - "invalid_api_key": "\u7121\u52b9\u306aAPI\u30ad\u30fc" - }, - "step": { - "reauth_confirm": { - "data": { - "api_key": "API\u30ad\u30fc", - "description": "Ambee\u30a2\u30ab\u30a6\u30f3\u30c8\u3067\u518d\u8a8d\u8a3c\u3057\u307e\u3059\u3002" - } - }, - "user": { - "data": { - "api_key": "API\u30ad\u30fc", - "latitude": "\u7def\u5ea6", - "longitude": "\u7d4c\u5ea6", - "name": "\u540d\u524d" - }, - "description": "Ambee \u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u3066\u3001Home Assistant\u3068\u9023\u643a\u3059\u308b\u3088\u3046\u306b\u3057\u307e\u3059\u3002" - } - } - }, - "issues": { - "pending_removal": { - "description": "Ambee\u306e\u7d71\u5408\u306fHome Assistant\u304b\u3089\u306e\u524a\u9664\u306f\u4fdd\u7559\u3055\u308c\u3066\u3044\u307e\u3059\u304c\u3001Home Assistant 2022.10\u4ee5\u964d\u306f\u5229\u7528\u3067\u304d\u306a\u304f\u306a\u308a\u307e\u3059\u3002 \n\nAmbee\u304c\u7121\u6599(\u9650\u5b9a)\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u524a\u9664\u3057\u3001\u4e00\u822c\u30e6\u30fc\u30b6\u30fc\u304c\u6709\u6599\u30d7\u30e9\u30f3\u306b\u30b5\u30a4\u30f3\u30a2\u30c3\u30d7\u3059\u308b\u65b9\u6cd5\u3092\u63d0\u4f9b\u3057\u306a\u304f\u306a\u3063\u305f\u305f\u3081\u3001\u7d71\u5408\u304c\u524a\u9664\u3055\u308c\u308b\u3053\u3068\u306b\u306a\u308a\u307e\u3057\u305f\u3002\n\n\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001\u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u304b\u3089Ambee\u306e\u7d71\u5408\u306e\u30a8\u30f3\u30c8\u30ea\u3092\u524a\u9664\u3057\u3066\u304f\u3060\u3055\u3044\u3002", - "title": "Ambee\u306e\u7d71\u5408\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/ko.json b/homeassistant/components/ambee/translations/ko.json deleted file mode 100644 index 574b7cd0976..00000000000 --- a/homeassistant/components/ambee/translations/ko.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "config": { - "error": { - "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", - "invalid_api_key": "API \ud0a4\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" - }, - "step": { - "user": { - "data": { - "api_key": "API \ud0a4", - "latitude": "\uc704\ub3c4", - "longitude": "\uacbd\ub3c4", - "name": "\uc774\ub984" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/nl.json b/homeassistant/components/ambee/translations/nl.json deleted file mode 100644 index 957c3547be2..00000000000 --- a/homeassistant/components/ambee/translations/nl.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "config": { - "abort": { - "reauth_successful": "Herauthenticatie geslaagd" - }, - "error": { - "cannot_connect": "Kan geen verbinding maken", - "invalid_api_key": "Ongeldige API-sleutel" - }, - "step": { - "reauth_confirm": { - "data": { - "api_key": "API-sleutel", - "description": "Verifieer opnieuw met uw Ambee-account." - } - }, - "user": { - "data": { - "api_key": "API-sleutel", - "latitude": "Breedtegraad", - "longitude": "Lengtegraad", - "name": "Naam" - }, - "description": "Stel Ambee in om te integreren met Home Assistant." - } - } - }, - "issues": { - "pending_removal": { - "title": "De Ambee-integratie wordt verwijderd" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/no.json b/homeassistant/components/ambee/translations/no.json deleted file mode 100644 index 2c10f596722..00000000000 --- a/homeassistant/components/ambee/translations/no.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "config": { - "abort": { - "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" - }, - "error": { - "cannot_connect": "Tilkobling mislyktes", - "invalid_api_key": "Ugyldig API-n\u00f8kkel" - }, - "step": { - "reauth_confirm": { - "data": { - "api_key": "API-n\u00f8kkel", - "description": "Autentiser p\u00e5 nytt med Ambee-kontoen din." - } - }, - "user": { - "data": { - "api_key": "API-n\u00f8kkel", - "latitude": "Breddegrad", - "longitude": "Lengdegrad", - "name": "Navn" - }, - "description": "Sett opp Ambee for \u00e5 integrere med Home Assistant." - } - } - }, - "issues": { - "pending_removal": { - "description": "Ambee-integrasjonen venter p\u00e5 fjerning fra Home Assistant og vil ikke lenger v\u00e6re tilgjengelig fra Home Assistant 2022.10. \n\n Integrasjonen blir fjernet, fordi Ambee fjernet deres gratis (begrensede) kontoer og ikke gir vanlige brukere mulighet til \u00e5 registrere seg for en betalt plan lenger. \n\n Fjern Ambee-integrasjonsoppf\u00f8ringen fra forekomsten din for \u00e5 fikse dette problemet.", - "title": "Ambee-integrasjonen blir fjernet" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/pl.json b/homeassistant/components/ambee/translations/pl.json deleted file mode 100644 index 255d402175d..00000000000 --- a/homeassistant/components/ambee/translations/pl.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "config": { - "abort": { - "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" - }, - "error": { - "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", - "invalid_api_key": "Nieprawid\u0142owy klucz API" - }, - "step": { - "reauth_confirm": { - "data": { - "api_key": "Klucz API", - "description": "Ponownie uwierzytelnij za pomoc\u0105 konta Ambee." - } - }, - "user": { - "data": { - "api_key": "Klucz API", - "latitude": "Szeroko\u015b\u0107 geograficzna", - "longitude": "D\u0142ugo\u015b\u0107 geograficzna", - "name": "Nazwa" - }, - "description": "Skonfiguruj Ambee, aby zintegrowa\u0107 go z Home Assistantem." - } - } - }, - "issues": { - "pending_removal": { - "description": "Integracja Ambee oczekuje na usuni\u0119cie z Home Assistanta i nie b\u0119dzie ju\u017c dost\u0119pna od Home Assistant 2022.10. \n\nIntegracja jest usuwana, poniewa\u017c Ambee usun\u0105\u0142 ich bezp\u0142atne (ograniczone) konta i nie zapewnia ju\u017c zwyk\u0142ym u\u017cytkownikom mo\u017cliwo\u015bci zarejestrowania si\u0119 w p\u0142atnym planie. \n\nUsu\u0144 integracj\u0119 Ambee z Home Assistanta, aby rozwi\u0105za\u0107 ten problem.", - "title": "Integracja Ambee zostanie usuni\u0119ta" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/pt-BR.json b/homeassistant/components/ambee/translations/pt-BR.json deleted file mode 100644 index 3220de5104e..00000000000 --- a/homeassistant/components/ambee/translations/pt-BR.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "config": { - "abort": { - "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" - }, - "error": { - "cannot_connect": "Falha ao conectar", - "invalid_api_key": "Chave de API inv\u00e1lida" - }, - "step": { - "reauth_confirm": { - "data": { - "api_key": "Chave da API", - "description": "Re-autentique com sua conta Ambee." - } - }, - "user": { - "data": { - "api_key": "Chave da API", - "latitude": "Latitude", - "longitude": "Longitude", - "name": "Nome" - }, - "description": "Configure o Ambee para integrar com o Home Assistant." - } - } - }, - "issues": { - "pending_removal": { - "description": "A integra\u00e7\u00e3o do Ambee est\u00e1 com remo\u00e7\u00e3o pendente do Home Assistant e n\u00e3o estar\u00e1 mais dispon\u00edvel a partir do Home Assistant 2022.10. \n\n A integra\u00e7\u00e3o est\u00e1 sendo removida, porque a Ambee removeu suas contas gratuitas (limitadas) e n\u00e3o oferece mais uma maneira de usu\u00e1rios regulares se inscreverem em um plano pago. \n\n Remova a entrada de integra\u00e7\u00e3o Ambee de sua inst\u00e2ncia para corrigir esse problema.", - "title": "A integra\u00e7\u00e3o Ambee est\u00e1 sendo removida" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/pt.json b/homeassistant/components/ambee/translations/pt.json deleted file mode 100644 index 4a6d267473b..00000000000 --- a/homeassistant/components/ambee/translations/pt.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "config": { - "step": { - "reauth_confirm": { - "data": { - "api_key": "Chave da API" - } - }, - "user": { - "data": { - "latitude": "Latitude", - "name": "Nome" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/ru.json b/homeassistant/components/ambee/translations/ru.json deleted file mode 100644 index 11b3cbbf9d2..00000000000 --- a/homeassistant/components/ambee/translations/ru.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "config": { - "abort": { - "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_api_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API." - }, - "step": { - "reauth_confirm": { - "data": { - "api_key": "\u041a\u043b\u044e\u0447 API", - "description": "\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 Ambee" - } - }, - "user": { - "data": { - "api_key": "\u041a\u043b\u044e\u0447 API", - "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430", - "longitude": "\u0414\u043e\u043b\u0433\u043e\u0442\u0430", - "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" - }, - "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 Ambee." - } - } - }, - "issues": { - "pending_removal": { - "description": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f Ambee \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\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430, \u043f\u043e\u0442\u043e\u043c\u0443 \u0447\u0442\u043e Ambee \u0443\u0434\u0430\u043b\u0438\u043b\u0430 \u0441\u0432\u043e\u0438 \u0431\u0435\u0441\u043f\u043b\u0430\u0442\u043d\u044b\u0435 (\u043e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d\u043d\u044b\u0435) \u0430\u043a\u043a\u0430\u0443\u043d\u0442\u044b \u0438 \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435 \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u0442 \u043e\u0431\u044b\u0447\u043d\u044b\u043c \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f\u043c \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u0438 \u043f\u043e\u0434\u043f\u0438\u0441\u0430\u0442\u044c\u0441\u044f \u043d\u0430 \u043f\u043b\u0430\u0442\u043d\u044b\u0439 \u0442\u0430\u0440\u0438\u0444\u043d\u044b\u0439 \u043f\u043b\u0430\u043d.\n\n\u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u043d\u0443\u044e \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044e, \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 Ambee \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/sensor.ca.json b/homeassistant/components/ambee/translations/sensor.ca.json deleted file mode 100644 index b85d6bdc8e2..00000000000 --- a/homeassistant/components/ambee/translations/sensor.ca.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "state": { - "ambee__risk": { - "high": "Alt", - "low": "Baix", - "moderate": "Moderat", - "very high": "Molt alt" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/sensor.de.json b/homeassistant/components/ambee/translations/sensor.de.json deleted file mode 100644 index c96a2c50eb7..00000000000 --- a/homeassistant/components/ambee/translations/sensor.de.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "state": { - "ambee__risk": { - "high": "Hoch", - "low": "Niedrig", - "moderate": "M\u00e4\u00dfig", - "very high": "Sehr hoch" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/sensor.el.json b/homeassistant/components/ambee/translations/sensor.el.json deleted file mode 100644 index 8e9af2dac05..00000000000 --- a/homeassistant/components/ambee/translations/sensor.el.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "state": { - "ambee__risk": { - "high": "\u03a5\u03c8\u03b7\u03bb\u03cc", - "low": "\u03a7\u03b1\u03bc\u03b7\u03bb\u03cc", - "moderate": "\u039c\u03ad\u03c4\u03c1\u03b9\u03bf", - "very high": "\u03a0\u03bf\u03bb\u03cd \u03c5\u03c8\u03b7\u03bb\u03cc" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/sensor.en.json b/homeassistant/components/ambee/translations/sensor.en.json deleted file mode 100644 index a4b198eadf5..00000000000 --- a/homeassistant/components/ambee/translations/sensor.en.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "state": { - "ambee__risk": { - "high": "High", - "low": "Low", - "moderate": "Moderate", - "very high": "Very High" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/sensor.es-419.json b/homeassistant/components/ambee/translations/sensor.es-419.json deleted file mode 100644 index a676ca7aa5e..00000000000 --- a/homeassistant/components/ambee/translations/sensor.es-419.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "state": { - "ambee__risk": { - "high": "Alto", - "low": "Bajo", - "moderate": "Moderado", - "very high": "Muy alto" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/sensor.es.json b/homeassistant/components/ambee/translations/sensor.es.json deleted file mode 100644 index a676ca7aa5e..00000000000 --- a/homeassistant/components/ambee/translations/sensor.es.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "state": { - "ambee__risk": { - "high": "Alto", - "low": "Bajo", - "moderate": "Moderado", - "very high": "Muy alto" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/sensor.et.json b/homeassistant/components/ambee/translations/sensor.et.json deleted file mode 100644 index 7599f2fd2c3..00000000000 --- a/homeassistant/components/ambee/translations/sensor.et.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "state": { - "ambee__risk": { - "high": "K\u00f5rge", - "low": "Madal", - "moderate": "M\u00f5\u00f5dukas", - "very high": "V\u00e4ga k\u00f5rge" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/sensor.fr.json b/homeassistant/components/ambee/translations/sensor.fr.json deleted file mode 100644 index 76dc3fe6301..00000000000 --- a/homeassistant/components/ambee/translations/sensor.fr.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "state": { - "ambee__risk": { - "high": "Haute", - "low": "Faible", - "moderate": "Mod\u00e9rer", - "very high": "Tr\u00e8s \u00e9lev\u00e9" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/sensor.he.json b/homeassistant/components/ambee/translations/sensor.he.json deleted file mode 100644 index 14ae06f2bc9..00000000000 --- a/homeassistant/components/ambee/translations/sensor.he.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "state": { - "ambee__risk": { - "high": "\u05d2\u05d1\u05d5\u05d4", - "low": "\u05e0\u05de\u05d5\u05da", - "very high": "\u05d2\u05d1\u05d5\u05d4 \u05de\u05d0\u05d5\u05d3" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/sensor.hu.json b/homeassistant/components/ambee/translations/sensor.hu.json deleted file mode 100644 index 975d200a507..00000000000 --- a/homeassistant/components/ambee/translations/sensor.hu.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "state": { - "ambee__risk": { - "high": "Magas", - "low": "Alacsony", - "moderate": "M\u00e9rs\u00e9kelt", - "very high": "Nagyon magas" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/sensor.id.json b/homeassistant/components/ambee/translations/sensor.id.json deleted file mode 100644 index 5cb74694da5..00000000000 --- a/homeassistant/components/ambee/translations/sensor.id.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "state": { - "ambee__risk": { - "high": "Tinggi", - "low": "Rendah", - "moderate": "Sedang", - "very high": "Sangat Tinggi" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/sensor.it.json b/homeassistant/components/ambee/translations/sensor.it.json deleted file mode 100644 index 1c265a6ca53..00000000000 --- a/homeassistant/components/ambee/translations/sensor.it.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "state": { - "ambee__risk": { - "high": "Alto", - "low": "Basso", - "moderate": "Moderato", - "very high": "Molto alto" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/sensor.ja.json b/homeassistant/components/ambee/translations/sensor.ja.json deleted file mode 100644 index a750a257864..00000000000 --- a/homeassistant/components/ambee/translations/sensor.ja.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "state": { - "ambee__risk": { - "high": "\u9ad8\u3044", - "low": "\u4f4e\u3044", - "moderate": "\u9069\u5ea6", - "very high": "\u975e\u5e38\u306b\u9ad8\u3044" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/sensor.nl.json b/homeassistant/components/ambee/translations/sensor.nl.json deleted file mode 100644 index e9ba0c76a34..00000000000 --- a/homeassistant/components/ambee/translations/sensor.nl.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "state": { - "ambee__risk": { - "high": "Hoog", - "low": "Laag", - "moderate": "Matig", - "very high": "Zeer hoog" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/sensor.no.json b/homeassistant/components/ambee/translations/sensor.no.json deleted file mode 100644 index cf4e4bed6ed..00000000000 --- a/homeassistant/components/ambee/translations/sensor.no.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "state": { - "ambee__risk": { - "high": "H\u00f8y", - "low": "Lav", - "moderate": "Moderat", - "very high": "Veldig h\u00f8y" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/sensor.pl.json b/homeassistant/components/ambee/translations/sensor.pl.json deleted file mode 100644 index d67bdec0879..00000000000 --- a/homeassistant/components/ambee/translations/sensor.pl.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "state": { - "ambee__risk": { - "high": "wysoki", - "low": "niski", - "moderate": "umiarkowany", - "very high": "bardzo wysoki" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/sensor.pt-BR.json b/homeassistant/components/ambee/translations/sensor.pt-BR.json deleted file mode 100644 index 2e0dc187368..00000000000 --- a/homeassistant/components/ambee/translations/sensor.pt-BR.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "state": { - "ambee__risk": { - "high": "Alto", - "low": "Baixo", - "moderate": "Moderado", - "very high": "Muito alto" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/sensor.ru.json b/homeassistant/components/ambee/translations/sensor.ru.json deleted file mode 100644 index c0dbe8cecd6..00000000000 --- a/homeassistant/components/ambee/translations/sensor.ru.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "state": { - "ambee__risk": { - "high": "\u0412\u044b\u0441\u043e\u043a\u0438\u0439", - "low": "\u041d\u0438\u0437\u043a\u0438\u0439", - "moderate": "\u0423\u043c\u0435\u0440\u0435\u043d\u043d\u044b\u0439", - "very high": "\u041e\u0447\u0435\u043d\u044c \u0432\u044b\u0441\u043e\u043a\u0438\u0439" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/sensor.sv.json b/homeassistant/components/ambee/translations/sensor.sv.json deleted file mode 100644 index d3280d4ebf4..00000000000 --- a/homeassistant/components/ambee/translations/sensor.sv.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "state": { - "ambee__risk": { - "high": "H\u00f6g", - "low": "L\u00e5g", - "moderate": "M\u00e5ttlig", - "very high": "V\u00e4ldigt h\u00f6gt" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/sensor.tr.json b/homeassistant/components/ambee/translations/sensor.tr.json deleted file mode 100644 index 087bea4ed99..00000000000 --- a/homeassistant/components/ambee/translations/sensor.tr.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "state": { - "ambee__risk": { - "high": "Y\u00fcksek", - "low": "D\u00fc\u015f\u00fck", - "moderate": "Moderate", - "very high": "\u00c7ok y\u00fcksek" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/sensor.zh-Hant.json b/homeassistant/components/ambee/translations/sensor.zh-Hant.json deleted file mode 100644 index 1e3c5bbe58d..00000000000 --- a/homeassistant/components/ambee/translations/sensor.zh-Hant.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "state": { - "ambee__risk": { - "high": "\u9ad8", - "low": "\u4f4e", - "moderate": "\u4e2d", - "very high": "\u6975\u9ad8" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/sk.json b/homeassistant/components/ambee/translations/sk.json deleted file mode 100644 index a474631a7f8..00000000000 --- a/homeassistant/components/ambee/translations/sk.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "config": { - "abort": { - "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" - }, - "error": { - "invalid_api_key": "Neplatn\u00fd API k\u013e\u00fa\u010d" - }, - "step": { - "reauth_confirm": { - "data": { - "api_key": "API k\u013e\u00fa\u010d" - } - }, - "user": { - "data": { - "api_key": "API k\u013e\u00fa\u010d", - "latitude": "Zemepisn\u00e1 \u0161\u00edrka", - "longitude": "Zemepisn\u00e1 d\u013a\u017eka", - "name": "N\u00e1zov" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/sv.json b/homeassistant/components/ambee/translations/sv.json deleted file mode 100644 index e31205118b9..00000000000 --- a/homeassistant/components/ambee/translations/sv.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "config": { - "abort": { - "reauth_successful": "\u00c5terautentisering lyckades" - }, - "error": { - "cannot_connect": "Det gick inte att ansluta.", - "invalid_api_key": "Ogiltig API-nyckel" - }, - "step": { - "reauth_confirm": { - "data": { - "api_key": "API-nyckel", - "description": "Autentisera p\u00e5 nytt med ditt Ambee-konto." - } - }, - "user": { - "data": { - "api_key": "API-nyckel", - "latitude": "Latitud", - "longitude": "Longitud", - "name": "Namn" - }, - "description": "Konfigurera Ambee f\u00f6r att integrera med Home Assistant." - } - } - }, - "issues": { - "pending_removal": { - "description": "Ambee-integrationen 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 Integrationen tas bort eftersom Ambee tog bort sina gratis (begr\u00e4nsade) konton och inte l\u00e4ngre ger vanliga anv\u00e4ndare m\u00f6jlighet att registrera sig f\u00f6r en betalplan. \n\n Ta bort Ambee-integreringsposten fr\u00e5n din instans f\u00f6r att \u00e5tg\u00e4rda problemet.", - "title": "Ambee-integrationen tas bort" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/tr.json b/homeassistant/components/ambee/translations/tr.json deleted file mode 100644 index 0163ea40bae..00000000000 --- a/homeassistant/components/ambee/translations/tr.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "config": { - "abort": { - "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" - }, - "error": { - "cannot_connect": "Ba\u011flanma hatas\u0131", - "invalid_api_key": "Ge\u00e7ersiz API anahtar\u0131" - }, - "step": { - "reauth_confirm": { - "data": { - "api_key": "API Anahtar\u0131", - "description": "Ambee hesab\u0131n\u0131zla yeniden kimlik do\u011frulamas\u0131 yap\u0131n." - } - }, - "user": { - "data": { - "api_key": "API Anahtar\u0131", - "latitude": "Enlem", - "longitude": "Boylam", - "name": "Ad" - }, - "description": "Ambee'yi Home Assistant ile entegre olacak \u015fekilde ayarlay\u0131n." - } - } - }, - "issues": { - "pending_removal": { - "description": "Ambee entegrasyonu Home Assistant'tan kald\u0131r\u0131lmay\u0131 beklemektedir ve Home Assistant 2022.10'dan itibaren art\u0131k kullan\u0131lamayacakt\u0131r.\n\nEntegrasyon kald\u0131r\u0131l\u0131yor \u00e7\u00fcnk\u00fc Ambee \u00fccretsiz (s\u0131n\u0131rl\u0131) hesaplar\u0131n\u0131 kald\u0131rd\u0131 ve art\u0131k normal kullan\u0131c\u0131lar\u0131n \u00fccretli bir plana kaydolmas\u0131 i\u00e7in bir yol sa\u011flam\u0131yor.\n\nBu sorunu gidermek i\u00e7in Ambee entegrasyon giri\u015fini \u00f6rne\u011finizden kald\u0131r\u0131n.", - "title": "Ambee entegrasyonu kald\u0131r\u0131l\u0131yor" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/zh-Hant.json b/homeassistant/components/ambee/translations/zh-Hant.json deleted file mode 100644 index ccebea49c6f..00000000000 --- a/homeassistant/components/ambee/translations/zh-Hant.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "config": { - "abort": { - "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" - }, - "error": { - "cannot_connect": "\u9023\u7dda\u5931\u6557", - "invalid_api_key": "API \u91d1\u9470\u7121\u6548" - }, - "step": { - "reauth_confirm": { - "data": { - "api_key": "API \u91d1\u9470", - "description": "\u91cd\u65b0\u8a8d\u8b49 Ambee \u5e33\u865f\u3002" - } - }, - "user": { - "data": { - "api_key": "API \u91d1\u9470", - "latitude": "\u7def\u5ea6", - "longitude": "\u7d93\u5ea6", - "name": "\u540d\u7a31" - }, - "description": "\u8a2d\u5b9a Ambee \u4ee5\u6574\u5408\u81f3 Home Assistant\u3002" - } - } - }, - "issues": { - "pending_removal": { - "description": "Ambee \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\u65bc Ambee \u79fb\u9664\u4e86\u5176\u514d\u8cbb\uff08\u6709\u9650\uff09\u5e33\u865f\u3001\u4e26\u4e14\u4e0d\u518d\u63d0\u4f9b\u4e00\u822c\u4f7f\u7528\u8005\u8a3b\u518a\u4ed8\u8cbb\u670d\u52d9\u3001\u6574\u5408\u5373\u5c07\u79fb\u9664\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": "Ambee \u6574\u5408\u5373\u5c07\u79fb\u9664" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/amberelectric/translations/bg.json b/homeassistant/components/amberelectric/translations/bg.json index 6f035fae4e6..f7765e3461f 100644 --- a/homeassistant/components/amberelectric/translations/bg.json +++ b/homeassistant/components/amberelectric/translations/bg.json @@ -1,5 +1,9 @@ { "config": { + "error": { + "invalid_api_token": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d API \u043a\u043b\u044e\u0447", + "unknown_error": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, "step": { "user": { "description": "\u041e\u0442\u0438\u0434\u0435\u0442\u0435 \u043d\u0430 {api_url}, \u0437\u0430 \u0434\u0430 \u0433\u0435\u043d\u0435\u0440\u0438\u0440\u0430\u0442\u0435 API \u043a\u043b\u044e\u0447" diff --git a/homeassistant/components/amberelectric/translations/ca.json b/homeassistant/components/amberelectric/translations/ca.json index 208c7fb9f7c..678a70f6db7 100644 --- a/homeassistant/components/amberelectric/translations/ca.json +++ b/homeassistant/components/amberelectric/translations/ca.json @@ -1,5 +1,10 @@ { "config": { + "error": { + "invalid_api_token": "Clau API inv\u00e0lida", + "no_site": "No s'ha proporcionat cap lloc", + "unknown_error": "Error inesperat" + }, "step": { "site": { "data": { diff --git a/homeassistant/components/amberelectric/translations/cs.json b/homeassistant/components/amberelectric/translations/cs.json new file mode 100644 index 00000000000..e2986983838 --- /dev/null +++ b/homeassistant/components/amberelectric/translations/cs.json @@ -0,0 +1,8 @@ +{ + "config": { + "error": { + "invalid_api_token": "Neplatn\u00fd kl\u00ed\u010d API", + "unknown_error": "Neo\u010dek\u00e1van\u00e1 chyba" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/amberelectric/translations/de.json b/homeassistant/components/amberelectric/translations/de.json index 34ce233fed8..333755501b9 100644 --- a/homeassistant/components/amberelectric/translations/de.json +++ b/homeassistant/components/amberelectric/translations/de.json @@ -1,5 +1,10 @@ { "config": { + "error": { + "invalid_api_token": "Ung\u00fcltiger API-Schl\u00fcssel", + "no_site": "Kein Standort vorhanden", + "unknown_error": "Unerwarteter Fehler" + }, "step": { "site": { "data": { diff --git a/homeassistant/components/amberelectric/translations/el.json b/homeassistant/components/amberelectric/translations/el.json index 3dc8fc9dd78..018a4d33bd3 100644 --- a/homeassistant/components/amberelectric/translations/el.json +++ b/homeassistant/components/amberelectric/translations/el.json @@ -1,5 +1,10 @@ { "config": { + "error": { + "invalid_api_token": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API", + "no_site": "\u0394\u03b5\u03bd \u03c0\u03b1\u03c1\u03ad\u03c7\u03b5\u03c4\u03b1\u03b9 \u03c7\u03ce\u03c1\u03bf\u03c2", + "unknown_error": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, "step": { "site": { "data": { diff --git a/homeassistant/components/amberelectric/translations/en.json b/homeassistant/components/amberelectric/translations/en.json index 0a974298134..3798aa77bf6 100644 --- a/homeassistant/components/amberelectric/translations/en.json +++ b/homeassistant/components/amberelectric/translations/en.json @@ -1,5 +1,10 @@ { "config": { + "error": { + "invalid_api_token": "Invalid API key", + "no_site": "No site provided", + "unknown_error": "Unexpected error" + }, "step": { "site": { "data": { @@ -15,11 +20,6 @@ }, "description": "Go to {api_url} to generate an API key" } - }, - "error": { - "invalid_api_token": "Invalid API key", - "no_site": "No site provided", - "unknown_error": "Unexpected error" } } } \ No newline at end of file diff --git a/homeassistant/components/amberelectric/translations/es.json b/homeassistant/components/amberelectric/translations/es.json index e8c40903e27..4e34117641b 100644 --- a/homeassistant/components/amberelectric/translations/es.json +++ b/homeassistant/components/amberelectric/translations/es.json @@ -1,5 +1,10 @@ { "config": { + "error": { + "invalid_api_token": "Clave API no v\u00e1lida", + "no_site": "No se proporciona ning\u00fan sitio", + "unknown_error": "Error inesperado" + }, "step": { "site": { "data": { diff --git a/homeassistant/components/amberelectric/translations/et.json b/homeassistant/components/amberelectric/translations/et.json index e48f6f2a749..0bab3d8661d 100644 --- a/homeassistant/components/amberelectric/translations/et.json +++ b/homeassistant/components/amberelectric/translations/et.json @@ -1,5 +1,10 @@ { "config": { + "error": { + "invalid_api_token": "Vigane API v\u00f5ti", + "no_site": "Saiti pole pakutud", + "unknown_error": "Ootamatu t\u00f5rge" + }, "step": { "site": { "data": { diff --git a/homeassistant/components/amberelectric/translations/fr.json b/homeassistant/components/amberelectric/translations/fr.json index a5b1164924c..2a23419258d 100644 --- a/homeassistant/components/amberelectric/translations/fr.json +++ b/homeassistant/components/amberelectric/translations/fr.json @@ -1,5 +1,10 @@ { "config": { + "error": { + "invalid_api_token": "Cl\u00e9 d'API non valide", + "no_site": "Aucun site fourni", + "unknown_error": "Erreur inattendue" + }, "step": { "site": { "data": { diff --git a/homeassistant/components/amberelectric/translations/he.json b/homeassistant/components/amberelectric/translations/he.json new file mode 100644 index 00000000000..8999f497df9 --- /dev/null +++ b/homeassistant/components/amberelectric/translations/he.json @@ -0,0 +1,8 @@ +{ + "config": { + "error": { + "invalid_api_token": "\u05de\u05e4\u05ea\u05d7 API \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "unknown_error": "\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/amberelectric/translations/hu.json b/homeassistant/components/amberelectric/translations/hu.json index 2d361e1e76f..3422f278ba8 100644 --- a/homeassistant/components/amberelectric/translations/hu.json +++ b/homeassistant/components/amberelectric/translations/hu.json @@ -1,5 +1,10 @@ { "config": { + "error": { + "invalid_api_token": "\u00c9rv\u00e9nytelen API kulcs", + "no_site": "Nincs megadva a helysz\u00edn", + "unknown_error": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, "step": { "site": { "data": { diff --git a/homeassistant/components/amberelectric/translations/id.json b/homeassistant/components/amberelectric/translations/id.json index a88fca7c520..85f96921714 100644 --- a/homeassistant/components/amberelectric/translations/id.json +++ b/homeassistant/components/amberelectric/translations/id.json @@ -1,5 +1,10 @@ { "config": { + "error": { + "invalid_api_token": "Kunci API tidak valid", + "no_site": "Tidak ada situs yang disediakan", + "unknown_error": "Kesalahan yang tidak diharapkan" + }, "step": { "site": { "data": { diff --git a/homeassistant/components/amberelectric/translations/it.json b/homeassistant/components/amberelectric/translations/it.json index f181f549fff..0a247160d92 100644 --- a/homeassistant/components/amberelectric/translations/it.json +++ b/homeassistant/components/amberelectric/translations/it.json @@ -1,5 +1,10 @@ { "config": { + "error": { + "invalid_api_token": "Chiave API non valida", + "no_site": "Nessun sito fornito", + "unknown_error": "Errore imprevisto" + }, "step": { "site": { "data": { diff --git a/homeassistant/components/amberelectric/translations/ja.json b/homeassistant/components/amberelectric/translations/ja.json index 0c061b26112..bc39623d399 100644 --- a/homeassistant/components/amberelectric/translations/ja.json +++ b/homeassistant/components/amberelectric/translations/ja.json @@ -1,5 +1,10 @@ { "config": { + "error": { + "invalid_api_token": "\u7121\u52b9\u306aAPI\u30ad\u30fc", + "no_site": "\u30b5\u30a4\u30c8\u306e\u63d0\u4f9b\u306a\u3057", + "unknown_error": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, "step": { "site": { "data": { diff --git a/homeassistant/components/amberelectric/translations/nl.json b/homeassistant/components/amberelectric/translations/nl.json index 263e94962a0..11f0576496e 100644 --- a/homeassistant/components/amberelectric/translations/nl.json +++ b/homeassistant/components/amberelectric/translations/nl.json @@ -1,5 +1,9 @@ { "config": { + "error": { + "invalid_api_token": "Ongeldige API-sleutel", + "unknown_error": "Onverwachte fout" + }, "step": { "site": { "data": { diff --git a/homeassistant/components/amberelectric/translations/no.json b/homeassistant/components/amberelectric/translations/no.json index 74df9323ce5..380a8cdeda9 100644 --- a/homeassistant/components/amberelectric/translations/no.json +++ b/homeassistant/components/amberelectric/translations/no.json @@ -1,5 +1,10 @@ { "config": { + "error": { + "invalid_api_token": "Ugyldig API-n\u00f8kkel", + "no_site": "Ingen side oppgitt", + "unknown_error": "Uventet feil" + }, "step": { "site": { "data": { diff --git a/homeassistant/components/amberelectric/translations/pl.json b/homeassistant/components/amberelectric/translations/pl.json index 2810149273f..9c7197a9ac0 100644 --- a/homeassistant/components/amberelectric/translations/pl.json +++ b/homeassistant/components/amberelectric/translations/pl.json @@ -1,5 +1,10 @@ { "config": { + "error": { + "invalid_api_token": "Nieprawid\u0142owy klucz API", + "no_site": "Nie podano strony", + "unknown_error": "Nieoczekiwany b\u0142\u0105d" + }, "step": { "site": { "data": { diff --git a/homeassistant/components/amberelectric/translations/pt-BR.json b/homeassistant/components/amberelectric/translations/pt-BR.json index 35541dbf290..17285172a30 100644 --- a/homeassistant/components/amberelectric/translations/pt-BR.json +++ b/homeassistant/components/amberelectric/translations/pt-BR.json @@ -1,5 +1,10 @@ { "config": { + "error": { + "invalid_api_token": "Chave de API inv\u00e1lida", + "no_site": "Nenhum site fornecido", + "unknown_error": "Erro inesperado" + }, "step": { "site": { "data": { diff --git a/homeassistant/components/amberelectric/translations/pt.json b/homeassistant/components/amberelectric/translations/pt.json new file mode 100644 index 00000000000..a58215237fb --- /dev/null +++ b/homeassistant/components/amberelectric/translations/pt.json @@ -0,0 +1,9 @@ +{ + "config": { + "error": { + "invalid_api_token": "Chave de API inv\u00e1lida", + "no_site": "Nenhum site fornecido", + "unknown_error": "Erro inesperado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/amberelectric/translations/ru.json b/homeassistant/components/amberelectric/translations/ru.json index 793f8bcae9d..f81b4db835c 100644 --- a/homeassistant/components/amberelectric/translations/ru.json +++ b/homeassistant/components/amberelectric/translations/ru.json @@ -1,5 +1,10 @@ { "config": { + "error": { + "invalid_api_token": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API.", + "no_site": "\u0421\u0430\u0439\u0442 \u043d\u0435 \u0443\u043a\u0430\u0437\u0430\u043d.", + "unknown_error": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, "step": { "site": { "data": { diff --git a/homeassistant/components/amberelectric/translations/tr.json b/homeassistant/components/amberelectric/translations/tr.json index 393b2cf08ee..ff8c80d8f51 100644 --- a/homeassistant/components/amberelectric/translations/tr.json +++ b/homeassistant/components/amberelectric/translations/tr.json @@ -1,5 +1,10 @@ { "config": { + "error": { + "invalid_api_token": "Ge\u00e7ersiz API anahtar\u0131", + "no_site": "Site sa\u011flanmad\u0131", + "unknown_error": "Beklenmeyen hata" + }, "step": { "site": { "data": { diff --git a/homeassistant/components/amberelectric/translations/zh-Hant.json b/homeassistant/components/amberelectric/translations/zh-Hant.json index 42b671d3530..121db7935fa 100644 --- a/homeassistant/components/amberelectric/translations/zh-Hant.json +++ b/homeassistant/components/amberelectric/translations/zh-Hant.json @@ -1,5 +1,10 @@ { "config": { + "error": { + "invalid_api_token": "API \u91d1\u9470\u7121\u6548", + "no_site": "\u672a\u63d0\u4f9b\u7ad9\u9ede", + "unknown_error": "\u672a\u9810\u671f\u932f\u8aa4" + }, "step": { "site": { "data": { diff --git a/homeassistant/components/ambiclimate/climate.py b/homeassistant/components/ambiclimate/climate.py index 99fefbb180e..5a5fea6c230 100644 --- a/homeassistant/components/ambiclimate/climate.py +++ b/homeassistant/components/ambiclimate/climate.py @@ -8,8 +8,11 @@ from typing import Any import ambiclimate import voluptuous as vol -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ClimateEntityFeature, HVACMode +from homeassistant.components.climate import ( + ClimateEntity, + ClimateEntityFeature, + HVACMode, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_NAME, diff --git a/homeassistant/components/ambiclimate/translations/fr.json b/homeassistant/components/ambiclimate/translations/fr.json index b6464b58244..2453bc16d07 100644 --- a/homeassistant/components/ambiclimate/translations/fr.json +++ b/homeassistant/components/ambiclimate/translations/fr.json @@ -9,7 +9,7 @@ "default": "Authentification r\u00e9ussie" }, "error": { - "follow_link": "Veuillez suivre le lien et vous authentifier avant d'appuyer sur Soumettre.", + "follow_link": "Veuillez suivre le lien et vous authentifier avant d'appuyer sur \u00ab\u00a0Valider\u00a0\u00bb", "no_token": "Non authentifi\u00e9 avec Ambiclimate" }, "step": { diff --git a/homeassistant/components/ambient_station/sensor.py b/homeassistant/components/ambient_station/sensor.py index a04c279915f..65c726bfff3 100644 --- a/homeassistant/components/ambient_station/sensor.py +++ b/homeassistant/components/ambient_station/sensor.py @@ -308,6 +308,7 @@ SENSOR_DESCRIPTIONS = ( name="Max gust", icon="mdi:weather-windy", native_unit_of_measurement=SPEED_MILES_PER_HOUR, + device_class=SensorDeviceClass.SPEED, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( @@ -631,6 +632,7 @@ SENSOR_DESCRIPTIONS = ( name="Wind gust", icon="mdi:weather-windy", native_unit_of_measurement=SPEED_MILES_PER_HOUR, + device_class=SensorDeviceClass.SPEED, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( @@ -638,18 +640,21 @@ SENSOR_DESCRIPTIONS = ( name="Wind avg 10m", icon="mdi:weather-windy", native_unit_of_measurement=SPEED_MILES_PER_HOUR, + device_class=SensorDeviceClass.SPEED, ), SensorEntityDescription( key=TYPE_WINDSPDMPH_AVG2M, name="Wind avg 2m", icon="mdi:weather-windy", native_unit_of_measurement=SPEED_MILES_PER_HOUR, + device_class=SensorDeviceClass.SPEED, ), SensorEntityDescription( key=TYPE_WINDSPEEDMPH, name="Wind speed", icon="mdi:weather-windy", native_unit_of_measurement=SPEED_MILES_PER_HOUR, + device_class=SensorDeviceClass.SPEED, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( diff --git a/homeassistant/components/amcrest/camera.py b/homeassistant/components/amcrest/camera.py index da5e046a88a..df742b320db 100644 --- a/homeassistant/components/amcrest/camera.py +++ b/homeassistant/components/amcrest/camera.py @@ -12,8 +12,11 @@ from amcrest import AmcrestError from haffmpeg.camera import CameraMjpeg import voluptuous as vol -from homeassistant.components.camera import Camera, CameraEntityFeature -from homeassistant.components.camera.const import DOMAIN as CAMERA_DOMAIN +from homeassistant.components.camera import ( + DOMAIN as CAMERA_DOMAIN, + Camera, + CameraEntityFeature, +) from homeassistant.components.ffmpeg import FFmpegManager, get_ffmpeg_manager from homeassistant.const import ATTR_ENTITY_ID, CONF_NAME, STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant diff --git a/homeassistant/components/analytics/analytics.py b/homeassistant/components/analytics/analytics.py index 1a696b0c206..5bb1836928c 100644 --- a/homeassistant/components/analytics/analytics.py +++ b/homeassistant/components/analytics/analytics.py @@ -8,7 +8,7 @@ import async_timeout from homeassistant.components import hassio from homeassistant.components.api import ATTR_INSTALLATION_TYPE -from homeassistant.components.automation.const import DOMAIN as AUTOMATION_DOMAIN +from homeassistant.components.automation import DOMAIN as AUTOMATION_DOMAIN from homeassistant.components.energy import ( DOMAIN as ENERGY_DOMAIN, is_configured as energy_is_configured, diff --git a/homeassistant/components/analytics/manifest.json b/homeassistant/components/analytics/manifest.json index b52d85f5496..34c23a94c6d 100644 --- a/homeassistant/components/analytics/manifest.json +++ b/homeassistant/components/analytics/manifest.json @@ -6,5 +6,6 @@ "dependencies": ["api", "websocket_api"], "after_dependencies": ["energy"], "quality_scale": "internal", - "iot_class": "cloud_push" + "iot_class": "cloud_push", + "integration_type": "system" } diff --git a/homeassistant/components/android_ip_webcam/translations/bg.json b/homeassistant/components/android_ip_webcam/translations/bg.json new file mode 100644 index 00000000000..7d31d58dcc0 --- /dev/null +++ b/homeassistant/components/android_ip_webcam/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" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435" + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "port": "\u041f\u043e\u0440\u0442", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/android_ip_webcam/translations/cs.json b/homeassistant/components/android_ip_webcam/translations/cs.json new file mode 100644 index 00000000000..543988bca9b --- /dev/null +++ b/homeassistant/components/android_ip_webcam/translations/cs.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed" + }, + "step": { + "user": { + "data": { + "host": "Hostitel", + "password": "Heslo", + "port": "Port", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/android_ip_webcam/translations/fr.json b/homeassistant/components/android_ip_webcam/translations/fr.json index 19f42be4376..6381ce09051 100644 --- a/homeassistant/components/android_ip_webcam/translations/fr.json +++ b/homeassistant/components/android_ip_webcam/translations/fr.json @@ -17,5 +17,10 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "title": "La configuration YAML pour webcam IP Android sera bient\u00f4t supprim\u00e9e" + } } } \ No newline at end of file diff --git a/homeassistant/components/android_ip_webcam/translations/nl.json b/homeassistant/components/android_ip_webcam/translations/nl.json index 37162761d86..e18be76b805 100644 --- a/homeassistant/components/android_ip_webcam/translations/nl.json +++ b/homeassistant/components/android_ip_webcam/translations/nl.json @@ -1,7 +1,21 @@ { "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd" + }, "error": { + "cannot_connect": "Kan geen verbinding maken", "invalid_auth": "Ongeldige authenticatie" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Wachtwoord", + "port": "Poort", + "username": "Gebruikersnaam" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/android_ip_webcam/translations/pt.json b/homeassistant/components/android_ip_webcam/translations/pt.json new file mode 100644 index 00000000000..795ba71964f --- /dev/null +++ b/homeassistant/components/android_ip_webcam/translations/pt.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, + "step": { + "user": { + "data": { + "host": "Servidor", + "password": "Palavra-passe", + "port": "Porta", + "username": "Nome de Utilizador" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/android_ip_webcam/translations/sv.json b/homeassistant/components/android_ip_webcam/translations/sv.json new file mode 100644 index 00000000000..9c15e903d9f --- /dev/null +++ b/homeassistant/components/android_ip_webcam/translations/sv.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering" + }, + "step": { + "user": { + "data": { + "host": "V\u00e4rd", + "password": "L\u00f6senord", + "port": "Port", + "username": "Anv\u00e4ndarnamn" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Konfigurering av Android IP-webbkamera med YAML tas bort. \n\n Din befintliga YAML-konfiguration har automatiskt importerats till anv\u00e4ndargr\u00e4nssnittet. \n\n Ta bort Android IP Webcam YAML-konfigurationen fr\u00e5n filen configuration.yaml och starta om Home Assistant f\u00f6r att \u00e5tg\u00e4rda problemet.", + "title": "Android IP Webcam YAML-konfigurationen tas bort" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/androidtv/media_player.py b/homeassistant/components/androidtv/media_player.py index 696fab5788f..241ac12e780 100644 --- a/homeassistant/components/androidtv/media_player.py +++ b/homeassistant/components/androidtv/media_player.py @@ -21,8 +21,10 @@ import voluptuous as vol from homeassistant.components import persistent_notification from homeassistant.components.media_player import ( + MediaPlayerDeviceClass, MediaPlayerEntity, MediaPlayerEntityFeature, + MediaPlayerState, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -33,11 +35,6 @@ from homeassistant.const import ( ATTR_SW_VERSION, CONF_HOST, CONF_NAME, - STATE_IDLE, - STATE_OFF, - STATE_PAUSED, - STATE_PLAYING, - STATE_STANDBY, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv, entity_platform @@ -85,11 +82,11 @@ PREFIX_FIRETV = "Fire TV" # Translate from `AndroidTV` / `FireTV` reported state to HA state. ANDROIDTV_STATES = { - "off": STATE_OFF, - "idle": STATE_IDLE, - "standby": STATE_STANDBY, - "playing": STATE_PLAYING, - "paused": STATE_PAUSED, + "off": MediaPlayerState.OFF, + "idle": MediaPlayerState.IDLE, + "standby": MediaPlayerState.STANDBY, + "playing": MediaPlayerState.PLAYING, + "paused": MediaPlayerState.PAUSED, } @@ -209,6 +206,8 @@ def adb_decorator( class ADBDevice(MediaPlayerEntity): """Representation of an Android TV or Fire TV device.""" + _attr_device_class = MediaPlayerDeviceClass.TV + def __init__( self, aftv, @@ -323,7 +322,11 @@ class ADBDevice(MediaPlayerEntity): async def async_get_media_image(self) -> tuple[bytes | None, str | None]: """Fetch current playing image.""" - if not self._screencap or self.state in (STATE_OFF, None) or not self.available: + if ( + not self._screencap + or self.state in {MediaPlayerState.OFF, None} + or not self.available + ): return None, None media_data = await self._adb_screencap() diff --git a/homeassistant/components/androidtv/translations/ja.json b/homeassistant/components/androidtv/translations/ja.json index 26b83a2643a..c9cabbc538b 100644 --- a/homeassistant/components/androidtv/translations/ja.json +++ b/homeassistant/components/androidtv/translations/ja.json @@ -41,12 +41,12 @@ "init": { "data": { "apps": "\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u30ea\u30b9\u30c8\u306e\u8a2d\u5b9a", - "exclude_unnamed_apps": "\u540d\u524d\u304c\u4e0d\u660e\u306a\u30a2\u30d7\u30ea\u3092\u9664\u5916\u3059\u308b", - "get_sources": "\u5b9f\u884c\u4e2d\u306e\u30a2\u30d7\u30ea\u3092\u30bd\u30fc\u30b9\u306e\u30ea\u30b9\u30c8\u3068\u3057\u3066\u53d6\u5f97\u3059\u308b\u304b\u3069\u3046\u304b", - "screencap": "\u753b\u9762\u306b\u8868\u793a\u4e2d\u306e\u3082\u306e\u304b\u3089\u3001\u30a2\u30eb\u30d0\u30e0\u30a2\u30fc\u30c8\u3092\u62bd\u51fa\u3059\u308b\u304b\u3069\u3046\u304b\u3092\u6c7a\u5b9a\u3057\u307e\u3059", + "exclude_unnamed_apps": "\u30bd\u30fc\u30b9 \u30ea\u30b9\u30c8\u304b\u3089\u4e0d\u660e\u306a\u540d\u524d\u306e\u30a2\u30d7\u30ea\u3092\u9664\u5916\u3059\u308b", + "get_sources": "\u5b9f\u884c\u4e2d\u306e\u30a2\u30d7\u30ea\u3092\u30bd\u30fc\u30b9\u306e\u30ea\u30b9\u30c8\u3068\u3057\u3066\u53d6\u5f97\u3059\u308b", + "screencap": "\u30a2\u30eb\u30d0\u30e0 \u30a2\u30fc\u30c8\u306b\u30b9\u30af\u30ea\u30fc\u30f3 \u30ad\u30e3\u30d7\u30c1\u30e3\u3092\u4f7f\u7528\u3059\u308b", "state_detection_rules": "\u72b6\u614b\u691c\u51fa\u30eb\u30fc\u30eb\u3092\u8a2d\u5b9a", - "turn_off_command": "\u30c7\u30d5\u30a9\u30eb\u30c8\u306eturn_off\u30b3\u30de\u30f3\u30c9\u3092\u4e0a\u66f8\u304d\u3059\u308bADB\u30b7\u30a7\u30eb\u30b3\u30de\u30f3\u30c9", - "turn_on_command": "\u30c7\u30d5\u30a9\u30eb\u30c8\u306eturn_on\u30b3\u30de\u30f3\u30c9\u3092\u4e0a\u66f8\u304d\u3059\u308bADB\u30b7\u30a7\u30eb\u30b3\u30de\u30f3\u30c9" + "turn_off_command": "ADB \u30b7\u30a7\u30eb\u306f\u3001\u30b3\u30de\u30f3\u30c9\u3092\u30aa\u30d5\u306b\u3057\u307e\u3059 (\u30c7\u30d5\u30a9\u30eb\u30c8\u3067\u306f\u7a7a\u306e\u307e\u307e\u306b\u3057\u3066\u304a\u304d\u307e\u3059)", + "turn_on_command": "ADB shell turn on \u30b3\u30de\u30f3\u30c9 (\u30c7\u30d5\u30a9\u30eb\u30c8\u3067\u306f\u7a7a\u306e\u307e\u307e)" } }, "rules": { diff --git a/homeassistant/components/androidtv/translations/pt.json b/homeassistant/components/androidtv/translations/pt.json index 0d9b37a78f0..adc29b49b38 100644 --- a/homeassistant/components/androidtv/translations/pt.json +++ b/homeassistant/components/androidtv/translations/pt.json @@ -1,8 +1,20 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, "error": { "cannot_connect": "Falha na liga\u00e7\u00e3o", - "invalid_host": "Nome de servidor ou endere\u00e7o IP inv\u00e1lido." + "invalid_host": "Nome de servidor ou endere\u00e7o IP inv\u00e1lido.", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "host": "Servidor", + "port": "Porta" + } + } } }, "options": { diff --git a/homeassistant/components/anthemav/media_player.py b/homeassistant/components/anthemav/media_player.py index a854ea0653e..0c5837e154e 100644 --- a/homeassistant/components/anthemav/media_player.py +++ b/homeassistant/components/anthemav/media_player.py @@ -12,16 +12,10 @@ from homeassistant.components.media_player import ( MediaPlayerDeviceClass, MediaPlayerEntity, MediaPlayerEntityFeature, + MediaPlayerState, ) from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import ( - CONF_HOST, - CONF_MAC, - CONF_NAME, - CONF_PORT, - STATE_OFF, - STATE_ON, -) +from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, CONF_PORT from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -163,7 +157,9 @@ class AnthemAVR(MediaPlayerEntity): def set_states(self) -> None: """Set all the states from the device to the entity.""" - self._attr_state = STATE_ON if self._zone.power is True else STATE_OFF + self._attr_state = ( + MediaPlayerState.ON if self._zone.power else MediaPlayerState.OFF + ) self._attr_is_volume_muted = self._zone.mute self._attr_volume_level = self._zone.volume_as_percentage self._attr_media_title = self._zone.input_name diff --git a/homeassistant/components/anthemav/translations/bg.json b/homeassistant/components/anthemav/translations/bg.json new file mode 100644 index 00000000000..cc5f200ef95 --- /dev/null +++ b/homeassistant/components/anthemav/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", + "port": "\u041f\u043e\u0440\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/cs.json b/homeassistant/components/anthemav/translations/cs.json new file mode 100644 index 00000000000..6fabc170b6e --- /dev/null +++ b/homeassistant/components/anthemav/translations/cs.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" + }, + "step": { + "user": { + "data": { + "host": "Hostitel" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/fr.json b/homeassistant/components/anthemav/translations/fr.json index faf417552ce..d08ef70d6b7 100644 --- a/homeassistant/components/anthemav/translations/fr.json +++ b/homeassistant/components/anthemav/translations/fr.json @@ -15,5 +15,10 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "title": "La configuration YAML pour les r\u00e9cepteurs A/V Anthem sera bient\u00f4t supprim\u00e9e" + } } } \ No newline at end of file diff --git a/homeassistant/components/apcupsd/__init__.py b/homeassistant/components/apcupsd/__init__.py index 7cbf33f8b47..1e76d070a48 100644 --- a/homeassistant/components/apcupsd/__init__.py +++ b/homeassistant/components/apcupsd/__init__.py @@ -1,11 +1,15 @@ """Support for APCUPSd via its Network Information Server (NIS).""" +from __future__ import annotations + from datetime import timedelta import logging +from typing import Any, Final from apcaccess import status import voluptuous as vol -from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.const import CONF_HOST, CONF_PORT, Platform from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType @@ -13,22 +17,18 @@ from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) -DEFAULT_HOST = "localhost" -DEFAULT_PORT = 3551 -DOMAIN = "apcupsd" +DOMAIN: Final = "apcupsd" +VALUE_ONLINE: Final = 8 +PLATFORMS: Final = (Platform.BINARY_SENSOR, Platform.SENSOR) +MIN_TIME_BETWEEN_UPDATES: Final = timedelta(seconds=60) -KEY_STATUS = "STATFLAG" - -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) - -VALUE_ONLINE = 8 CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.Schema( { - vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_HOST, default="localhost"): cv.string, + vol.Optional(CONF_PORT, default=3551): cv.port, } ) }, @@ -36,25 +36,67 @@ CONFIG_SCHEMA = vol.Schema( ) -def setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Use config values to set up a function enabling status retrieval.""" - conf = config[DOMAIN] - host = conf[CONF_HOST] - port = conf[CONF_PORT] +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up integration from legacy YAML configurations.""" + conf = config.get(DOMAIN) + if conf is None: + return True - apcups_data = APCUPSdData(host, port) - hass.data[DOMAIN] = apcups_data + # We only import configs from YAML if it hasn't been imported. If there is a config + # entry marked with SOURCE_IMPORT, it means the YAML config has been imported. + for entry in hass.config_entries.async_entries(DOMAIN): + if entry.source == SOURCE_IMPORT: + return True - # It doesn't really matter why we're not able to get the status, just that - # we can't. - try: - apcups_data.update(no_throttle=True) - except Exception: # pylint: disable=broad-except - _LOGGER.exception("Failure while testing APCUPSd status retrieval") - return False + # Since the YAML configuration for apcupsd consists of two parts: + # apcupsd: + # host: xxx + # port: xxx + # sensor: + # - platform: apcupsd + # resource: + # - resource_1 + # - resource_2 + # - ... + # Here at the integration set up we do not have the entire information to be + # imported to config flow yet. So we temporarily store the configuration to + # hass.data[DOMAIN] under a special entry_id SOURCE_IMPORT (which shouldn't + # conflict with other entry ids). Later when the sensor platform setup is + # called we gather the resources information and from there we start the + # actual config entry imports. + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][SOURCE_IMPORT] = conf return True +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Use config values to set up a function enabling status retrieval.""" + data_service = APCUPSdData( + config_entry.data[CONF_HOST], config_entry.data[CONF_PORT] + ) + + try: + await hass.async_add_executor_job(data_service.update) + except OSError as ex: + _LOGGER.error("Failure while testing APCUPSd status retrieval: %s", ex) + return False + + # Store the data service object. + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][config_entry.entry_id] = data_service + + # Forward the config entries to the supported platforms. + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + hass.data[DOMAIN].pop(entry.entry_id) + return unload_ok + + class APCUPSdData: """Stores the data retrieved from APCUPSd. @@ -62,26 +104,52 @@ class APCUPSdData: updates from the server. """ - def __init__(self, host, port): + def __init__(self, host: str, port: int) -> None: """Initialize the data object.""" - self._host = host self._port = port - self._status = None - self._get = status.get - self._parse = status.parse + self.status: dict[str, Any] = {} @property - def status(self): - """Get latest update if throttle allows. Return status.""" - self.update() - return self._status + def name(self) -> str | None: + """Return the name of the UPS, if available.""" + return self.status.get("UPSNAME") - def _get_status(self): - """Get the status from APCUPSd and parse it into a dict.""" - return self._parse(self._get(host=self._host, port=self._port)) + @property + def model(self) -> str | None: + """Return the model of the UPS, if available.""" + # Different UPS models may report slightly different keys for model, here we + # try them all. + for model_key in ("APCMODEL", "MODEL"): + if model_key in self.status: + return self.status[model_key] + return None + + @property + def sw_version(self) -> str | None: + """Return the software version of the APCUPSd, if available.""" + return self.status.get("VERSION") + + @property + def hw_version(self) -> str | None: + """Return the firmware version of the UPS, if available.""" + return self.status.get("FIRMWARE") + + @property + def serial_no(self) -> str | None: + """Return the unique serial number of the UPS, if available.""" + return self.status.get("SERIALNO") + + @property + def statflag(self) -> str | None: + """Return the STATFLAG indicating the status of the UPS, if available.""" + return self.status.get("STATFLAG") @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self, **kwargs): - """Fetch the latest status from APCUPSd.""" - self._status = self._get_status() + """Fetch the latest status from APCUPSd. + + Note that the result dict uses upper case for each resource, where our + integration uses lower cases as keys internally. + """ + self.status = status.parse(status.get(host=self._host, port=self._port)) diff --git a/homeassistant/components/apcupsd/binary_sensor.py b/homeassistant/components/apcupsd/binary_sensor.py index 25d50c06c97..7438e3236d4 100644 --- a/homeassistant/components/apcupsd/binary_sensor.py +++ b/homeassistant/components/apcupsd/binary_sensor.py @@ -1,43 +1,75 @@ """Support for tracking the online status of a UPS.""" from __future__ import annotations -import voluptuous as vol +import logging -from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity -from homeassistant.const import CONF_NAME +from homeassistant.components.binary_sensor import ( + BinarySensorEntity, + BinarySensorEntityDescription, +) +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import DOMAIN, KEY_STATUS, VALUE_ONLINE +from . import DOMAIN, VALUE_ONLINE, APCUPSdData -DEFAULT_NAME = "UPS Online Status" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - {vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string} +_LOGGER = logging.getLogger(__name__) +_DESCRIPTION = BinarySensorEntityDescription( + key="statflag", + name="UPS Online Status", + icon="mdi:heart", ) -def setup_platform( +async def async_setup_entry( hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up an APCUPSd Online Status binary sensor.""" - apcups_data = hass.data[DOMAIN] + data_service: APCUPSdData = hass.data[DOMAIN][config_entry.entry_id] - add_entities([OnlineStatus(config, apcups_data)], True) + # Do not create the binary sensor if APCUPSd does not provide STATFLAG field for us + # to determine the online status. + if data_service.statflag is None: + return + + async_add_entities( + [OnlineStatus(data_service, _DESCRIPTION)], + update_before_add=True, + ) class OnlineStatus(BinarySensorEntity): - """Representation of an UPS online status.""" + """Representation of a UPS online status.""" - def __init__(self, config, data): + def __init__( + self, + data_service: APCUPSdData, + description: BinarySensorEntityDescription, + ) -> None: """Initialize the APCUPSd binary device.""" - self._data = data - self._attr_name = config[CONF_NAME] + # Set up unique id and device info if serial number is available. + if (serial_no := data_service.serial_no) is not None: + self._attr_unique_id = f"{serial_no}_{description.key}" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, serial_no)}, + model=data_service.model, + manufacturer="APC", + hw_version=data_service.hw_version, + sw_version=data_service.sw_version, + ) + self.entity_description = description + self._data_service = data_service def update(self) -> None: """Get the status report from APCUPSd and set this entity's state.""" - self._attr_is_on = int(self._data.status[KEY_STATUS], 16) & VALUE_ONLINE > 0 + self._data_service.update() + + key = self.entity_description.key.upper() + if key not in self._data_service.status: + self._attr_is_on = None + return + + self._attr_is_on = int(self._data_service.status[key], 16) & VALUE_ONLINE > 0 diff --git a/homeassistant/components/apcupsd/config_flow.py b/homeassistant/components/apcupsd/config_flow.py new file mode 100644 index 00000000000..a191cf77117 --- /dev/null +++ b/homeassistant/components/apcupsd/config_flow.py @@ -0,0 +1,86 @@ +"""Config flow for APCUPSd integration.""" +from __future__ import annotations + +from typing import Any + +import voluptuous as vol + +from homeassistant.config_entries import ConfigFlow +from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers import selector +import homeassistant.helpers.config_validation as cv + +from . import DOMAIN, APCUPSdData + +_PORT_SELECTOR = vol.All( + selector.NumberSelector( + selector.NumberSelectorConfig( + min=1, max=65535, mode=selector.NumberSelectorMode.BOX + ), + ), + vol.Coerce(int), +) + +_SCHEMA = vol.Schema( + { + vol.Required(CONF_HOST, default="localhost"): cv.string, + vol.Required(CONF_PORT, default=3551): _PORT_SELECTOR, + } +) + + +class ConfigFlowHandler(ConfigFlow, domain=DOMAIN): + """APCUPSd integration config flow.""" + + 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=_SCHEMA) + + # Abort if an entry with same host and port is present. + self._async_abort_entries_match( + {CONF_HOST: user_input[CONF_HOST], CONF_PORT: user_input[CONF_PORT]} + ) + + # Test the connection to the host and get the current status for serial number. + data_service = APCUPSdData(user_input[CONF_HOST], user_input[CONF_PORT]) + try: + await self.hass.async_add_executor_job(data_service.update) + except OSError: + errors = {"base": "cannot_connect"} + return self.async_show_form( + step_id="user", data_schema=_SCHEMA, errors=errors + ) + + if not data_service.status: + return self.async_abort(reason="no_status") + + # We _try_ to use the serial number of the UPS as the unique id since this field + # is not guaranteed to exist on all APC UPS models. + await self.async_set_unique_id(data_service.serial_no) + self._abort_if_unique_id_configured() + + title = "APC UPS" + if data_service.name is not None: + title = data_service.name + elif data_service.model is not None: + title = data_service.model + elif data_service.serial_no is not None: + title = data_service.serial_no + + return self.async_create_entry( + title=title, + data=user_input, + ) + + async def async_step_import(self, conf: dict[str, Any]) -> FlowResult: + """Import a configuration from yaml configuration.""" + # If we are importing from YAML configuration, user_input could contain a + # CONF_RESOURCES with a list of resources (sensors) to be enabled. + return await self.async_step_user(user_input=conf) diff --git a/homeassistant/components/apcupsd/manifest.json b/homeassistant/components/apcupsd/manifest.json index 13a08685c68..aeead681e1b 100644 --- a/homeassistant/components/apcupsd/manifest.json +++ b/homeassistant/components/apcupsd/manifest.json @@ -1,9 +1,10 @@ { "domain": "apcupsd", - "name": "apcupsd", + "name": "APC UPS Daemon", + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/apcupsd", "requirements": ["apcaccess==0.0.13"], - "codeowners": [], + "codeowners": ["@yuxincs"], "iot_class": "local_polling", "loggers": ["apcaccess"] } diff --git a/homeassistant/components/apcupsd/sensor.py b/homeassistant/components/apcupsd/sensor.py index 0f85c4c6854..5f76e6c1afa 100644 --- a/homeassistant/components/apcupsd/sensor.py +++ b/homeassistant/components/apcupsd/sensor.py @@ -12,7 +12,10 @@ from homeassistant.components.sensor import ( SensorEntity, SensorEntityDescription, ) +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( + CONF_HOST, + CONF_PORT, CONF_RESOURCES, ELECTRIC_CURRENT_AMPERE, ELECTRIC_POTENTIAL_VOLT, @@ -26,368 +29,387 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv +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 . import DOMAIN +from . import DOMAIN, APCUPSdData _LOGGER = logging.getLogger(__name__) -SENSOR_PREFIX = "UPS " -SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( - SensorEntityDescription( +SENSORS: dict[str, SensorEntityDescription] = { + "alarmdel": SensorEntityDescription( key="alarmdel", - name="Alarm Delay", + name="UPS Alarm Delay", icon="mdi:alarm", ), - SensorEntityDescription( + "ambtemp": SensorEntityDescription( key="ambtemp", - name="Ambient Temperature", + name="UPS Ambient Temperature", icon="mdi:thermometer", ), - SensorEntityDescription( + "apc": SensorEntityDescription( key="apc", - name="Status Data", + name="UPS Status Data", icon="mdi:information-outline", + entity_registry_enabled_default=False, ), - SensorEntityDescription( + "apcmodel": SensorEntityDescription( key="apcmodel", - name="Model", + name="UPS Model", icon="mdi:information-outline", + entity_registry_enabled_default=False, ), - SensorEntityDescription( + "badbatts": SensorEntityDescription( key="badbatts", - name="Bad Batteries", + name="UPS Bad Batteries", icon="mdi:information-outline", ), - SensorEntityDescription( + "battdate": SensorEntityDescription( key="battdate", - name="Battery Replaced", + name="UPS Battery Replaced", icon="mdi:calendar-clock", ), - SensorEntityDescription( + "battstat": SensorEntityDescription( key="battstat", - name="Battery Status", + name="UPS Battery Status", icon="mdi:information-outline", ), - SensorEntityDescription( + "battv": SensorEntityDescription( key="battv", - name="Battery Voltage", + name="UPS Battery Voltage", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, icon="mdi:flash", ), - SensorEntityDescription( + "bcharge": SensorEntityDescription( key="bcharge", - name="Battery", + name="UPS Battery", native_unit_of_measurement=PERCENTAGE, icon="mdi:battery", ), - SensorEntityDescription( + "cable": SensorEntityDescription( key="cable", - name="Cable Type", + name="UPS Cable Type", icon="mdi:ethernet-cable", + entity_registry_enabled_default=False, ), - SensorEntityDescription( + "cumonbatt": SensorEntityDescription( key="cumonbatt", - name="Total Time on Battery", + name="UPS Total Time on Battery", icon="mdi:timer-outline", ), - SensorEntityDescription( + "date": SensorEntityDescription( key="date", - name="Status Date", + name="UPS Status Date", icon="mdi:calendar-clock", + entity_registry_enabled_default=False, ), - SensorEntityDescription( + "dipsw": SensorEntityDescription( key="dipsw", - name="Dip Switch Settings", + name="UPS Dip Switch Settings", icon="mdi:information-outline", ), - SensorEntityDescription( + "dlowbatt": SensorEntityDescription( key="dlowbatt", - name="Low Battery Signal", + name="UPS Low Battery Signal", icon="mdi:clock-alert", ), - SensorEntityDescription( + "driver": SensorEntityDescription( key="driver", - name="Driver", + name="UPS Driver", icon="mdi:information-outline", + entity_registry_enabled_default=False, ), - SensorEntityDescription( + "dshutd": SensorEntityDescription( key="dshutd", - name="Shutdown Delay", + name="UPS Shutdown Delay", icon="mdi:timer-outline", ), - SensorEntityDescription( + "dwake": SensorEntityDescription( key="dwake", - name="Wake Delay", + name="UPS Wake Delay", icon="mdi:timer-outline", ), - SensorEntityDescription( + "end apc": SensorEntityDescription( key="end apc", - name="Date and Time", + name="UPS Date and Time", icon="mdi:calendar-clock", + entity_registry_enabled_default=False, ), - SensorEntityDescription( + "extbatts": SensorEntityDescription( key="extbatts", - name="External Batteries", + name="UPS External Batteries", icon="mdi:information-outline", ), - SensorEntityDescription( + "firmware": SensorEntityDescription( key="firmware", - name="Firmware Version", + name="UPS Firmware Version", icon="mdi:information-outline", + entity_registry_enabled_default=False, ), - SensorEntityDescription( + "hitrans": SensorEntityDescription( key="hitrans", - name="Transfer High", + name="UPS Transfer High", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, icon="mdi:flash", ), - SensorEntityDescription( + "hostname": SensorEntityDescription( key="hostname", - name="Hostname", + name="UPS Hostname", icon="mdi:information-outline", + entity_registry_enabled_default=False, ), - SensorEntityDescription( + "humidity": SensorEntityDescription( key="humidity", - name="Ambient Humidity", + name="UPS Ambient Humidity", native_unit_of_measurement=PERCENTAGE, icon="mdi:water-percent", ), - SensorEntityDescription( + "itemp": SensorEntityDescription( key="itemp", - name="Internal Temperature", + name="UPS Internal Temperature", native_unit_of_measurement=TEMP_CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, ), - SensorEntityDescription( + "lastxfer": SensorEntityDescription( key="lastxfer", - name="Last Transfer", + name="UPS Last Transfer", icon="mdi:transfer", + entity_registry_enabled_default=False, ), - SensorEntityDescription( + "linefail": SensorEntityDescription( key="linefail", - name="Input Voltage Status", + name="UPS Input Voltage Status", icon="mdi:information-outline", ), - SensorEntityDescription( + "linefreq": SensorEntityDescription( key="linefreq", - name="Line Frequency", + name="UPS Line Frequency", native_unit_of_measurement=FREQUENCY_HERTZ, icon="mdi:information-outline", ), - SensorEntityDescription( + "linev": SensorEntityDescription( key="linev", - name="Input Voltage", + name="UPS Input Voltage", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, icon="mdi:flash", ), - SensorEntityDescription( + "loadpct": SensorEntityDescription( key="loadpct", - name="Load", + name="UPS Load", native_unit_of_measurement=PERCENTAGE, icon="mdi:gauge", ), - SensorEntityDescription( + "loadapnt": SensorEntityDescription( key="loadapnt", - name="Load Apparent Power", + name="UPS Load Apparent Power", native_unit_of_measurement=PERCENTAGE, icon="mdi:gauge", ), - SensorEntityDescription( + "lotrans": SensorEntityDescription( key="lotrans", - name="Transfer Low", + name="UPS Transfer Low", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, icon="mdi:flash", ), - SensorEntityDescription( + "mandate": SensorEntityDescription( key="mandate", - name="Manufacture Date", + name="UPS Manufacture Date", icon="mdi:calendar", + entity_registry_enabled_default=False, ), - SensorEntityDescription( + "masterupd": SensorEntityDescription( key="masterupd", - name="Master Update", + name="UPS Master Update", icon="mdi:information-outline", ), - SensorEntityDescription( + "maxlinev": SensorEntityDescription( key="maxlinev", - name="Input Voltage High", + name="UPS Input Voltage High", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, icon="mdi:flash", ), - SensorEntityDescription( + "maxtime": SensorEntityDescription( key="maxtime", - name="Battery Timeout", + name="UPS Battery Timeout", icon="mdi:timer-off-outline", ), - SensorEntityDescription( + "mbattchg": SensorEntityDescription( key="mbattchg", - name="Battery Shutdown", + name="UPS Battery Shutdown", native_unit_of_measurement=PERCENTAGE, icon="mdi:battery-alert", ), - SensorEntityDescription( + "minlinev": SensorEntityDescription( key="minlinev", - name="Input Voltage Low", + name="UPS Input Voltage Low", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, icon="mdi:flash", ), - SensorEntityDescription( + "mintimel": SensorEntityDescription( key="mintimel", - name="Shutdown Time", + name="UPS Shutdown Time", icon="mdi:timer-outline", ), - SensorEntityDescription( + "model": SensorEntityDescription( key="model", - name="Model", + name="UPS Model", icon="mdi:information-outline", + entity_registry_enabled_default=False, ), - SensorEntityDescription( + "nombattv": SensorEntityDescription( key="nombattv", - name="Battery Nominal Voltage", + name="UPS Battery Nominal Voltage", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, icon="mdi:flash", ), - SensorEntityDescription( + "nominv": SensorEntityDescription( key="nominv", - name="Nominal Input Voltage", + name="UPS Nominal Input Voltage", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, icon="mdi:flash", ), - SensorEntityDescription( + "nomoutv": SensorEntityDescription( key="nomoutv", - name="Nominal Output Voltage", + name="UPS Nominal Output Voltage", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, icon="mdi:flash", ), - SensorEntityDescription( + "nompower": SensorEntityDescription( key="nompower", - name="Nominal Output Power", + name="UPS Nominal Output Power", native_unit_of_measurement=POWER_WATT, icon="mdi:flash", ), - SensorEntityDescription( + "nomapnt": SensorEntityDescription( key="nomapnt", - name="Nominal Apparent Power", + name="UPS Nominal Apparent Power", native_unit_of_measurement=POWER_VOLT_AMPERE, icon="mdi:flash", ), - SensorEntityDescription( + "numxfers": SensorEntityDescription( key="numxfers", - name="Transfer Count", + name="UPS Transfer Count", icon="mdi:counter", ), - SensorEntityDescription( + "outcurnt": SensorEntityDescription( key="outcurnt", - name="Output Current", + name="UPS Output Current", native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, icon="mdi:flash", ), - SensorEntityDescription( + "outputv": SensorEntityDescription( key="outputv", - name="Output Voltage", + name="UPS Output Voltage", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, icon="mdi:flash", ), - SensorEntityDescription( + "reg1": SensorEntityDescription( key="reg1", - name="Register 1 Fault", + name="UPS Register 1 Fault", icon="mdi:information-outline", + entity_registry_enabled_default=False, ), - SensorEntityDescription( + "reg2": SensorEntityDescription( key="reg2", - name="Register 2 Fault", + name="UPS Register 2 Fault", icon="mdi:information-outline", + entity_registry_enabled_default=False, ), - SensorEntityDescription( + "reg3": SensorEntityDescription( key="reg3", - name="Register 3 Fault", + name="UPS Register 3 Fault", icon="mdi:information-outline", + entity_registry_enabled_default=False, ), - SensorEntityDescription( + "retpct": SensorEntityDescription( key="retpct", - name="Restore Requirement", + name="UPS Restore Requirement", native_unit_of_measurement=PERCENTAGE, icon="mdi:battery-alert", ), - SensorEntityDescription( + "selftest": SensorEntityDescription( key="selftest", - name="Last Self Test", + name="UPS Last Self Test", icon="mdi:calendar-clock", ), - SensorEntityDescription( + "sense": SensorEntityDescription( key="sense", - name="Sensitivity", + name="UPS Sensitivity", icon="mdi:information-outline", + entity_registry_enabled_default=False, ), - SensorEntityDescription( + "serialno": SensorEntityDescription( key="serialno", - name="Serial Number", + name="UPS Serial Number", icon="mdi:information-outline", + entity_registry_enabled_default=False, ), - SensorEntityDescription( + "starttime": SensorEntityDescription( key="starttime", - name="Startup Time", + name="UPS Startup Time", icon="mdi:calendar-clock", ), - SensorEntityDescription( + "statflag": SensorEntityDescription( key="statflag", - name="Status Flag", + name="UPS Status Flag", icon="mdi:information-outline", + entity_registry_enabled_default=False, ), - SensorEntityDescription( + "status": SensorEntityDescription( key="status", - name="Status", + name="UPS Status", icon="mdi:information-outline", ), - SensorEntityDescription( + "stesti": SensorEntityDescription( key="stesti", - name="Self Test Interval", + name="UPS Self Test Interval", icon="mdi:information-outline", ), - SensorEntityDescription( + "timeleft": SensorEntityDescription( key="timeleft", - name="Time Left", + name="UPS Time Left", icon="mdi:clock-alert", ), - SensorEntityDescription( + "tonbatt": SensorEntityDescription( key="tonbatt", - name="Time on Battery", + name="UPS Time on Battery", icon="mdi:timer-outline", ), - SensorEntityDescription( + "upsmode": SensorEntityDescription( key="upsmode", - name="Mode", + name="UPS Mode", icon="mdi:information-outline", ), - SensorEntityDescription( + "upsname": SensorEntityDescription( key="upsname", - name="Name", + name="UPS Name", icon="mdi:information-outline", + entity_registry_enabled_default=False, ), - SensorEntityDescription( + "version": SensorEntityDescription( key="version", - name="Daemon Info", + name="UPS Daemon Info", icon="mdi:information-outline", + entity_registry_enabled_default=False, ), - SensorEntityDescription( + "xoffbat": SensorEntityDescription( key="xoffbat", - name="Transfer from Battery", + name="UPS Transfer from Battery", icon="mdi:transfer", ), - SensorEntityDescription( + "xoffbatt": SensorEntityDescription( key="xoffbatt", - name="Transfer from Battery", + name="UPS Transfer from Battery", icon="mdi:transfer", ), - SensorEntityDescription( + "xonbatt": SensorEntityDescription( key="xonbatt", - name="Transfer to Battery", + name="UPS Transfer to Battery", icon="mdi:transfer", ), -) -SENSOR_KEYS: list[str] = [desc.key for desc in SENSOR_TYPES] +} SPECIFIC_UNITS = {"ITEMP": TEMP_CELSIUS} INFERRED_UNITS = { @@ -406,36 +428,109 @@ INFERRED_UNITS = { PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_RESOURCES, default=[]): vol.All( - cv.ensure_list, [vol.In(SENSOR_KEYS)] + cv.ensure_list, [vol.In([desc.key for desc in SENSORS.values()])] ) } ) -def setup_platform( +async def async_setup_platform( hass: HomeAssistant, config: ConfigType, add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: - """Set up the APCUPSd sensors.""" - apcups_data = hass.data[DOMAIN] - resources = config[CONF_RESOURCES] + """Import the configurations from YAML to config flows.""" + # We only import configs from YAML if it hasn't been imported. If there is a config + # entry marked with SOURCE_IMPORT, it means the YAML config has been imported. + for entry in hass.config_entries.async_entries(DOMAIN): + if entry.source == SOURCE_IMPORT: + return - for resource in resources: - if resource.upper() not in apcups_data.status: - _LOGGER.warning( - "Sensor type: %s does not appear in the APCUPSd status output", - resource, - ) + # This is the second step of YAML config imports, first see the comments in + # async_setup() of __init__.py to get an idea of how we import the YAML configs. + # Here we retrieve the partial YAML configs from the special entry id. + conf = hass.data[DOMAIN].get(SOURCE_IMPORT) + if conf is None: + return - entities = [ - APCUPSdSensor(apcups_data, description) - for description in SENSOR_TYPES - if description.key in resources - ] + _LOGGER.warning( + "Configuration of apcupsd in YAML is deprecated and will be " + "removed in Home Assistant 2022.12; Your existing configuration " + "has been imported into the UI automatically and can be safely removed " + "from your configuration.yaml file" + ) - add_entities(entities, True) + async_create_issue( + hass, + DOMAIN, + "deprecated_yaml", + breaks_in_ha_version="2022.12.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + ) + + # Remove the artificial entry since it's no longer needed. + hass.data[DOMAIN].pop(SOURCE_IMPORT) + + # Our config flow supports CONF_RESOURCES and will properly import it to disable + # entities not listed in CONF_RESOURCES by default. Note that this designed to + # support YAML config import only (i.e., not shown in UI during setup). + conf[CONF_RESOURCES] = config[CONF_RESOURCES] + + _LOGGER.debug( + "YAML configurations loaded with host %s, port %s and resources %s", + conf[CONF_HOST], + conf[CONF_PORT], + conf[CONF_RESOURCES], + ) + + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=conf + ) + ) + + return + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the APCUPSd sensors from config entries.""" + data_service: APCUPSdData = hass.data[DOMAIN][config_entry.entry_id] + + # The resources from data service are in upper-case by default, but we use + # lower cases throughout this integration. + available_resources: set[str] = {k.lower() for k, _ in data_service.status.items()} + + # We use user-specified resources from imported YAML config (if available) to + # determine whether to enable the entity by default. Here, we first collect the + # specified resources + specified_resources = None + if (resources := config_entry.data.get(CONF_RESOURCES)) is not None: + assert isinstance(resources, list) + specified_resources = set(resources) + + entities = [] + for resource in available_resources: + if resource not in SENSORS: + _LOGGER.warning("Invalid resource from APCUPSd: %s", resource.upper()) + continue + + # To avoid breaking changes, we disable sensors not specified in resources. + description = SENSORS[resource] + enabled_by_default = description.entity_registry_enabled_default + if specified_resources is not None: + enabled_by_default = resource in specified_resources + + entity = APCUPSdSensor(data_service, description, enabled_by_default) + entities.append(entity) + + async_add_entities(entities, update_before_add=True) def infer_unit(value): @@ -454,18 +549,39 @@ def infer_unit(value): class APCUPSdSensor(SensorEntity): """Representation of a sensor entity for APCUPSd status values.""" - def __init__(self, data, description: SensorEntityDescription): + def __init__( + self, + data_service: APCUPSdData, + description: SensorEntityDescription, + enabled_by_default: bool, + ) -> None: """Initialize the sensor.""" + # Set up unique id and device info if serial number is available. + if (serial_no := data_service.serial_no) is not None: + self._attr_unique_id = f"{serial_no}_{description.key}" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, serial_no)}, + model=data_service.model, + manufacturer="APC", + hw_version=data_service.hw_version, + sw_version=data_service.sw_version, + ) + self.entity_description = description - self._data = data - self._attr_name = f"{SENSOR_PREFIX}{description.name}" + self._attr_entity_registry_enabled_default = enabled_by_default + self._data_service = data_service def update(self) -> None: """Get the latest status and use it to update our sensor state.""" + self._data_service.update() + key = self.entity_description.key.upper() - if key not in self._data.status: + if key not in self._data_service.status: self._attr_native_value = None - else: - self._attr_native_value, inferred_unit = infer_unit(self._data.status[key]) - if not self.native_unit_of_measurement: - self._attr_native_unit_of_measurement = inferred_unit + return + + self._attr_native_value, inferred_unit = infer_unit( + self._data_service.status[key] + ) + if not self.native_unit_of_measurement: + self._attr_native_unit_of_measurement = inferred_unit diff --git a/homeassistant/components/apcupsd/strings.json b/homeassistant/components/apcupsd/strings.json new file mode 100644 index 00000000000..1ca53c0e854 --- /dev/null +++ b/homeassistant/components/apcupsd/strings.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "no_status": "No status is reported from [%key:common::config_flow::data::host%]" + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + }, + "step": { + "user": { + "data": { + "host": "[%key:common::config_flow::data::host%]", + "port": "[%key:common::config_flow::data::port%]" + }, + "description": "Enter the host and port on which the apcupsd NIS is being served." + } + } + }, + "issues": { + "deprecated_yaml": { + "title": "The APC UPS Daemon YAML configuration is being removed", + "description": "Configuring APC UPS Daemon using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the APC UPS Daemon YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } + } +} diff --git a/homeassistant/components/apcupsd/translations/en.json b/homeassistant/components/apcupsd/translations/en.json new file mode 100644 index 00000000000..3674f3c69fe --- /dev/null +++ b/homeassistant/components/apcupsd/translations/en.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured", + "no_status": "No status is reported from Host" + }, + "error": { + "cannot_connect": "Failed to connect" + }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Port" + }, + "description": "Enter the host and port on which the apcupsd NIS is being served." + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Configuring APC UPS Daemon using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the APC UPS Daemon YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "title": "The APC UPS Daemon YAML configuration is being removed" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/api/manifest.json b/homeassistant/components/api/manifest.json index 1f400470943..dadfc95c3b9 100644 --- a/homeassistant/components/api/manifest.json +++ b/homeassistant/components/api/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/api", "dependencies": ["http"], "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "system" } diff --git a/homeassistant/components/apple_tv/browse_media.py b/homeassistant/components/apple_tv/browse_media.py index 0673c9923fb..9944c49a823 100644 --- a/homeassistant/components/apple_tv/browse_media.py +++ b/homeassistant/components/apple_tv/browse_media.py @@ -1,34 +1,29 @@ """Support for media browsing.""" +from typing import Any -from homeassistant.components.media_player import BrowseMedia -from homeassistant.components.media_player.const import ( - MEDIA_CLASS_APP, - MEDIA_CLASS_DIRECTORY, - MEDIA_TYPE_APP, - MEDIA_TYPE_APPS, -) +from homeassistant.components.media_player import BrowseMedia, MediaClass, MediaType -def build_app_list(app_list): +def build_app_list(app_list: dict[str, str]) -> BrowseMedia: """Create response payload for app list.""" - app_list = [ - {"app_id": app_id, "title": app_name, "type": MEDIA_TYPE_APP} + media_list = [ + {"app_id": app_id, "title": app_name, "type": MediaType.APP} for app_name, app_id in app_list.items() ] return BrowseMedia( - media_class=MEDIA_CLASS_DIRECTORY, + media_class=MediaClass.DIRECTORY, media_content_id="apps", - media_content_type=MEDIA_TYPE_APPS, + media_content_type=MediaType.APPS, title="Apps", can_play=False, can_expand=True, - children=[item_payload(item) for item in app_list], - children_media_class=MEDIA_CLASS_APP, + children=[item_payload(item) for item in media_list], + children_media_class=MediaClass.APP, ) -def item_payload(item): +def item_payload(item: dict[str, Any]) -> BrowseMedia: """ Create response payload for a single media item. @@ -36,8 +31,8 @@ def item_payload(item): """ return BrowseMedia( title=item["title"], - media_class=MEDIA_CLASS_APP, - media_content_type=MEDIA_TYPE_APP, + media_class=MediaClass.APP, + media_content_type=MediaType.APP, media_content_id=item["app_id"], can_play=False, can_expand=False, diff --git a/homeassistant/components/apple_tv/media_player.py b/homeassistant/components/apple_tv/media_player.py index 771b27a6dc3..06618e4f2a3 100644 --- a/homeassistant/components/apple_tv/media_player.py +++ b/homeassistant/components/apple_tv/media_player.py @@ -1,6 +1,7 @@ """Support for Apple TV media player.""" from __future__ import annotations +from datetime import datetime import logging from typing import Any @@ -9,45 +10,31 @@ from pyatv.const import ( DeviceState, FeatureName, FeatureState, - MediaType, + MediaType as AppleMediaType, PowerState, RepeatState, ShuffleState, ) from pyatv.helpers import is_streamable +from pyatv.interface import AppleTV, Playing from homeassistant.components import media_source from homeassistant.components.media_player import ( BrowseMedia, MediaPlayerEntity, MediaPlayerEntityFeature, -) -from homeassistant.components.media_player.browse_media import ( + MediaPlayerState, + MediaType, + RepeatMode, async_process_play_media_url, ) -from homeassistant.components.media_player.const import ( - MEDIA_TYPE_APP, - MEDIA_TYPE_MUSIC, - MEDIA_TYPE_TVSHOW, - MEDIA_TYPE_VIDEO, - REPEAT_MODE_ALL, - REPEAT_MODE_OFF, - REPEAT_MODE_ONE, -) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - CONF_NAME, - STATE_IDLE, - STATE_OFF, - STATE_PAUSED, - STATE_PLAYING, - STATE_STANDBY, -) +from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback import homeassistant.util.dt as dt_util -from . import AppleTVEntity +from . import AppleTVEntity, AppleTVManager from .browse_media import build_app_list from .const import DOMAIN @@ -108,8 +95,9 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Load Apple TV media player based on a config entry.""" - name = config_entry.data[CONF_NAME] - manager = hass.data[DOMAIN][config_entry.unique_id] + name: str = config_entry.data[CONF_NAME] + assert config_entry.unique_id is not None + manager: AppleTVManager = hass.data[DOMAIN][config_entry.unique_id] async_add_entities([AppleTvMediaPlayer(name, config_entry.unique_id, manager)]) @@ -118,14 +106,14 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity): _attr_supported_features = SUPPORT_APPLE_TV - def __init__(self, name, identifier, manager, **kwargs): + def __init__(self, name: str, identifier: str, manager: AppleTVManager) -> None: """Initialize the Apple TV media player.""" - super().__init__(name, identifier, manager, **kwargs) - self._playing = None - self._app_list = {} + super().__init__(name, identifier, manager) + self._playing: Playing | None = None + self._app_list: dict[str, str] = {} @callback - def async_device_connected(self, atv): + def async_device_connected(self, atv: AppleTV) -> None: """Handle when connection is made to device.""" # NB: Do not use _is_feature_available here as it only works when playing if self.atv.features.in_state(FeatureState.Available, FeatureName.PushUpdates): @@ -153,7 +141,7 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity): if self.atv.features.in_state(FeatureState.Available, FeatureName.AppList): self.hass.create_task(self._update_app_list()) - async def _update_app_list(self): + async def _update_app_list(self) -> None: _LOGGER.debug("Updating app list") try: apps = await self.atv.apps.app_list() @@ -165,127 +153,128 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity): self._app_list = { app.name: app.identifier for app in sorted(apps, key=lambda app: app.name.lower()) + if app.name is not None } self.async_write_ha_state() @callback - def async_device_disconnected(self): + def async_device_disconnected(self) -> None: """Handle when connection was lost to device.""" self._attr_supported_features = SUPPORT_APPLE_TV @property - def state(self): + def state(self) -> MediaPlayerState | None: """Return the state of the device.""" if self.manager.is_connecting: return None if self.atv is None: - return STATE_OFF + return MediaPlayerState.OFF if ( self._is_feature_available(FeatureName.PowerState) and self.atv.power.power_state == PowerState.Off ): - return STATE_STANDBY + return MediaPlayerState.STANDBY if self._playing: state = self._playing.device_state if state in (DeviceState.Idle, DeviceState.Loading): - return STATE_IDLE + return MediaPlayerState.IDLE if state == DeviceState.Playing: - return STATE_PLAYING + return MediaPlayerState.PLAYING if state in (DeviceState.Paused, DeviceState.Seeking, DeviceState.Stopped): - return STATE_PAUSED - return STATE_STANDBY # Bad or unknown state? + return MediaPlayerState.PAUSED + return MediaPlayerState.STANDBY # Bad or unknown state? return None @callback - def playstatus_update(self, _, playing): + def playstatus_update(self, _, playing: Playing) -> None: """Print what is currently playing when it changes.""" self._playing = playing self.async_write_ha_state() @callback - def playstatus_error(self, _, exception): + def playstatus_error(self, _, exception: Exception) -> None: """Inform about an error and restart push updates.""" _LOGGER.warning("A %s error occurred: %s", exception.__class__, exception) self._playing = None self.async_write_ha_state() @callback - def powerstate_update(self, old_state: PowerState, new_state: PowerState): + def powerstate_update(self, old_state: PowerState, new_state: PowerState) -> None: """Update power state when it changes.""" self.async_write_ha_state() @property - def app_id(self): + def app_id(self) -> str | None: """ID of the current running app.""" if self._is_feature_available(FeatureName.App): return self.atv.metadata.app.identifier return None @property - def app_name(self): + def app_name(self) -> str | None: """Name of the current running app.""" if self._is_feature_available(FeatureName.App): return self.atv.metadata.app.name return None @property - def source_list(self): + def source_list(self) -> list[str]: """List of available input sources.""" return list(self._app_list.keys()) @property - def media_content_type(self): + def media_content_type(self) -> MediaType | None: """Content type of current playing media.""" if self._playing: return { - MediaType.Video: MEDIA_TYPE_VIDEO, - MediaType.Music: MEDIA_TYPE_MUSIC, - MediaType.TV: MEDIA_TYPE_TVSHOW, + AppleMediaType.Video: MediaType.VIDEO, + AppleMediaType.Music: MediaType.MUSIC, + AppleMediaType.TV: MediaType.TVSHOW, }.get(self._playing.media_type) return None @property - def media_content_id(self): + def media_content_id(self) -> str | None: """Content ID of current playing media.""" if self._playing: return self._playing.content_identifier return None @property - def volume_level(self): + def volume_level(self) -> float | None: """Volume level of the media player (0..1).""" if self._is_feature_available(FeatureName.Volume): return self.atv.audio.volume / 100.0 # from percent return None @property - def media_duration(self): + def media_duration(self) -> int | None: """Duration of current playing media in seconds.""" if self._playing: return self._playing.total_time return None @property - def media_position(self): + def media_position(self) -> int | None: """Position of current playing media in seconds.""" if self._playing: return self._playing.position return None @property - def media_position_updated_at(self): + def media_position_updated_at(self) -> datetime | None: """Last valid time of media position.""" - if self.state in (STATE_PLAYING, STATE_PAUSED): + if self.state in {MediaPlayerState.PLAYING, MediaPlayerState.PAUSED}: return dt_util.utcnow() return None 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_media command to the media player.""" # If input (file) has a file format supported by pyatv, then stream it with # RAOP. Otherwise try to play it with regular AirPlay. - if media_type == MEDIA_TYPE_APP: + if media_type == MediaType.APP: await self.atv.apps.launch_app(media_id) return @@ -294,10 +283,10 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity): self.hass, media_id, self.entity_id ) media_id = async_process_play_media_url(self.hass, play_item.url) - media_type = MEDIA_TYPE_MUSIC + media_type = MediaType.MUSIC if self._is_feature_available(FeatureName.StreamFile) and ( - media_type == MEDIA_TYPE_MUSIC or await is_streamable(media_id) + media_type == MediaType.MUSIC or await is_streamable(media_id) ): _LOGGER.debug("Streaming %s via RAOP", media_id) await self.atv.stream.stream_file(media_id) @@ -308,13 +297,13 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity): _LOGGER.error("Media streaming is not possible with current configuration") @property - def media_image_hash(self): + def media_image_hash(self) -> str | None: """Hash value for media image.""" state = self.state if ( self._playing and self._is_feature_available(FeatureName.Artwork) - and state not in [None, STATE_OFF, STATE_IDLE] + and state not in {None, MediaPlayerState.OFF, MediaPlayerState.IDLE} ): return self.atv.metadata.artwork_id return None @@ -322,7 +311,7 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity): async def async_get_media_image(self) -> tuple[bytes | None, str | None]: """Fetch media image of current playing image.""" state = self.state - if self._playing and state not in [STATE_OFF, STATE_IDLE]: + if self._playing and state not in {MediaPlayerState.OFF, MediaPlayerState.IDLE}: artwork = await self.atv.metadata.artwork() if artwork: return artwork.bytes, artwork.mimetype @@ -330,65 +319,65 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity): return None, None @property - def media_title(self): + def media_title(self) -> str | None: """Title of current playing media.""" if self._playing: return self._playing.title return None @property - def media_artist(self): + def media_artist(self) -> str | None: """Artist of current playing media, music track only.""" - if self._is_feature_available(FeatureName.Artist): + if self._playing and self._is_feature_available(FeatureName.Artist): return self._playing.artist return None @property - def media_album_name(self): + def media_album_name(self) -> str | None: """Album name of current playing media, music track only.""" - if self._is_feature_available(FeatureName.Album): + if self._playing and self._is_feature_available(FeatureName.Album): return self._playing.album return None @property - def media_series_title(self): + def media_series_title(self) -> str | None: """Title of series of current playing media, TV show only.""" - if self._is_feature_available(FeatureName.SeriesName): + if self._playing and self._is_feature_available(FeatureName.SeriesName): return self._playing.series_name return None @property - def media_season(self): + def media_season(self) -> str | None: """Season of current playing media, TV show only.""" - if self._is_feature_available(FeatureName.SeasonNumber): + if self._playing and self._is_feature_available(FeatureName.SeasonNumber): return str(self._playing.season_number) return None @property - def media_episode(self): + def media_episode(self) -> str | None: """Episode of current playing media, TV show only.""" - if self._is_feature_available(FeatureName.EpisodeNumber): + if self._playing and self._is_feature_available(FeatureName.EpisodeNumber): return str(self._playing.episode_number) return None @property - def repeat(self): + def repeat(self) -> RepeatMode | None: """Return current repeat mode.""" - if self._is_feature_available(FeatureName.Repeat): + if self._playing and self._is_feature_available(FeatureName.Repeat): return { - RepeatState.Track: REPEAT_MODE_ONE, - RepeatState.All: REPEAT_MODE_ALL, - }.get(self._playing.repeat, REPEAT_MODE_OFF) + RepeatState.Track: RepeatMode.ONE, + RepeatState.All: RepeatMode.ALL, + }.get(self._playing.repeat, RepeatMode.OFF) return None @property - def shuffle(self): + def shuffle(self) -> bool | None: """Boolean if shuffle is enabled.""" - if self._is_feature_available(FeatureName.Shuffle): + if self._playing and self._is_feature_available(FeatureName.Shuffle): return self._playing.shuffle != ShuffleState.Off return None - def _is_feature_available(self, feature): + def _is_feature_available(self, feature: FeatureName) -> bool: """Return if a feature is available.""" if self.atv and self._playing: return self.atv.features.in_state(FeatureState.Available, feature) @@ -396,7 +385,7 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity): async def async_browse_media( self, - media_content_type: str | None = None, + media_content_type: MediaType | str | None = None, media_content_id: str | None = None, ) -> BrowseMedia: """Implement the websocket media browsing helper.""" @@ -496,12 +485,12 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity): # pyatv expects volume in percent await self.atv.audio.set_volume(volume * 100.0) - async def async_set_repeat(self, repeat: str) -> None: + async def async_set_repeat(self, repeat: RepeatMode) -> None: """Set repeat mode.""" if self.atv: mode = { - REPEAT_MODE_ONE: RepeatState.Track, - REPEAT_MODE_ALL: RepeatState.All, + RepeatMode.ONE: RepeatState.Track, + RepeatMode.ALL: RepeatState.All, }.get(repeat, RepeatState.Off) await self.atv.remote_control.set_repeat(mode) diff --git a/homeassistant/components/apple_tv/translations/es.json b/homeassistant/components/apple_tv/translations/es.json index 3881692d0be..1f6966605cc 100644 --- a/homeassistant/components/apple_tv/translations/es.json +++ b/homeassistant/components/apple_tv/translations/es.json @@ -9,7 +9,7 @@ "inconsistent_device": "No se encontraron los protocolos esperados durante el descubrimiento. Esto normalmente indica un problema con multicast DNS (Zeroconf). Por favor, intenta a\u00f1adir el dispositivo de nuevo.", "ipv6_not_supported": "IPv6 no es compatible.", "no_devices_found": "No se encontraron dispositivos en la red", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente", "setup_failed": "No se pudo configurar el dispositivo.", "unknown": "Error inesperado" }, diff --git a/homeassistant/components/apple_tv/translations/pt.json b/homeassistant/components/apple_tv/translations/pt.json index e54e421caa5..d05409a4587 100644 --- a/homeassistant/components/apple_tv/translations/pt.json +++ b/homeassistant/components/apple_tv/translations/pt.json @@ -1,8 +1,10 @@ { "config": { "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", "no_devices_found": "Nenhum dispositivo encontrado na rede", + "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida", "unknown": "Erro inesperado" }, "error": { @@ -28,6 +30,9 @@ "description": "\u00c9 necess\u00e1rio fazer o emparelhamento com protocolo `{protocol}`. Insira o c\u00f3digo PIN exibido no ecran. Os zeros iniciais devem ser omitidos, ou seja, digite 123 se o c\u00f3digo exibido for 0123.", "title": "Emparelhamento" }, + "protocol_disabled": { + "title": "N\u00e3o \u00e9 poss\u00edvel emparelhar" + }, "reconfigure": { "description": "Esta Apple TV apresenta dificuldades de liga\u00e7\u00e3o e precisa ser reconfigurada.", "title": "Reconfigura\u00e7\u00e3o do dispositivo" diff --git a/homeassistant/components/application_credentials/__init__.py b/homeassistant/components/application_credentials/__init__.py index 6dd2d562307..811a637b4ef 100644 --- a/homeassistant/components/application_credentials/__init__.py +++ b/homeassistant/components/application_credentials/__init__.py @@ -15,6 +15,7 @@ import voluptuous as vol from homeassistant.components import websocket_api from homeassistant.components.websocket_api.connection import ActiveConnection +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_CLIENT_ID, CONF_CLIENT_SECRET, @@ -127,9 +128,7 @@ class ApplicationCredentialsStorageCollection(collection.StorageCollection): for item in self.async_items(): if item[CONF_DOMAIN] != domain: continue - auth_domain = ( - item[CONF_AUTH_DOMAIN] if CONF_AUTH_DOMAIN in item else item[CONF_ID] - ) + auth_domain = item.get(CONF_AUTH_DOMAIN, item[CONF_ID]) credentials[auth_domain] = ClientCredential( client_id=item[CONF_CLIENT_ID], client_secret=item[CONF_CLIENT_SECRET], @@ -156,6 +155,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: ).async_setup(hass) websocket_api.async_register_command(hass, handle_integration_list) + websocket_api.async_register_command(hass, handle_config_entry) config_entry_oauth2_flow.async_add_implementation_provider( hass, DOMAIN, _async_provide_implementation @@ -234,6 +234,27 @@ async def _async_provide_implementation( ] +async def _async_config_entry_app_credentials( + hass: HomeAssistant, + config_entry: ConfigEntry, +) -> str | None: + """Return the item id of an application credential for an existing ConfigEntry.""" + if not await _get_platform(hass, config_entry.domain) or not ( + auth_domain := config_entry.data.get("auth_implementation") + ): + return None + + storage_collection = hass.data[DOMAIN][DATA_STORAGE] + for item in storage_collection.async_items(): + item_id = item[CONF_ID] + if ( + item[CONF_DOMAIN] == config_entry.domain + and item.get(CONF_AUTH_DOMAIN, item_id) == auth_domain + ): + return item_id + return None + + class ApplicationCredentialsProtocol(Protocol): """Define the format that application_credentials platforms may have. @@ -311,3 +332,31 @@ async def handle_integration_list( }, } connection.send_result(msg["id"], result) + + +@websocket_api.websocket_command( + { + vol.Required("type"): "application_credentials/config_entry", + vol.Required("config_entry_id"): str, + } +) +@websocket_api.async_response +async def handle_config_entry( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: + """Return application credentials information for a config entry.""" + entry_id = msg["config_entry_id"] + config_entry = hass.config_entries.async_get_entry(entry_id) + if not config_entry: + connection.send_error( + msg["id"], + "invalid_config_entry_id", + f"Config entry not found: {entry_id}", + ) + return + result = {} + if application_credentials_id := await _async_config_entry_app_credentials( + hass, config_entry + ): + result["application_credentials_id"] = application_credentials_id + connection.send_result(msg["id"], result) diff --git a/homeassistant/components/application_credentials/manifest.json b/homeassistant/components/application_credentials/manifest.json index 9a8abc16c36..fa45f1a6309 100644 --- a/homeassistant/components/application_credentials/manifest.json +++ b/homeassistant/components/application_credentials/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/application_credentials", "dependencies": ["auth", "websocket_api"], "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "system" } diff --git a/homeassistant/components/aquostv/media_player.py b/homeassistant/components/aquostv/media_player.py index b0ff674e2de..34d5e4161fb 100644 --- a/homeassistant/components/aquostv/media_player.py +++ b/homeassistant/components/aquostv/media_player.py @@ -10,6 +10,7 @@ from homeassistant.components.media_player import ( PLATFORM_SCHEMA, MediaPlayerEntity, MediaPlayerEntityFeature, + MediaPlayerState, ) from homeassistant.const import ( CONF_HOST, @@ -18,8 +19,6 @@ from homeassistant.const import ( CONF_PORT, CONF_TIMEOUT, CONF_USERNAME, - STATE_OFF, - STATE_ON, ) from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -93,7 +92,7 @@ def _retry(func): except (OSError, TypeError, ValueError): update_retries -= 1 if update_retries == 0: - obj.set_state(STATE_OFF) + obj.set_state(MediaPlayerState.OFF) return wrapper @@ -134,9 +133,9 @@ class SharpAquosTVDevice(MediaPlayerEntity): def update(self) -> None: """Retrieve the latest data.""" if self._remote.power() == 1: - self._attr_state = STATE_ON + self._attr_state = MediaPlayerState.ON else: - self._attr_state = STATE_OFF + self._attr_state = MediaPlayerState.OFF # Set TV to be able to remotely power on if self._power_on_enabled: self._remote.power_on_command_settings(2) diff --git a/homeassistant/components/arcam_fmj/media_player.py b/homeassistant/components/arcam_fmj/media_player.py index f995b79df04..65a5d8c3580 100644 --- a/homeassistant/components/arcam_fmj/media_player.py +++ b/homeassistant/components/arcam_fmj/media_player.py @@ -9,17 +9,15 @@ from arcam.fmj.state import State from homeassistant.components.media_player import ( BrowseMedia, + MediaClass, MediaPlayerEntity, MediaPlayerEntityFeature, -) -from homeassistant.components.media_player.const import ( - MEDIA_CLASS_DIRECTORY, - MEDIA_CLASS_MUSIC, - MEDIA_TYPE_MUSIC, + MediaPlayerState, + MediaType, ) from homeassistant.components.media_player.errors import BrowseError from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON +from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo @@ -91,11 +89,11 @@ class ArcamFmj(MediaPlayerEntity): self._attr_entity_registry_enabled_default = state.zn == 1 @property - def state(self): + def state(self) -> MediaPlayerState: """Return the state of the device.""" if self._state.get_power(): - return STATE_ON - return STATE_OFF + return MediaPlayerState.ON + return MediaPlayerState.OFF @property def device_info(self): @@ -202,7 +200,9 @@ class ArcamFmj(MediaPlayerEntity): await self._state.set_power(False) async def async_browse_media( - self, media_content_type: str | None = None, media_content_id: str | None = None + self, + media_content_type: MediaType | str | None = None, + media_content_id: str | None = None, ) -> BrowseMedia: """Implement the websocket media browsing helper.""" if media_content_id not in (None, "root"): @@ -215,9 +215,9 @@ class ArcamFmj(MediaPlayerEntity): radio = [ BrowseMedia( title=preset.name, - media_class=MEDIA_CLASS_MUSIC, + media_class=MediaClass.MUSIC, media_content_id=f"preset:{preset.index}", - media_content_type=MEDIA_TYPE_MUSIC, + media_content_type=MediaType.MUSIC, can_play=True, can_expand=False, ) @@ -226,7 +226,7 @@ class ArcamFmj(MediaPlayerEntity): root = BrowseMedia( title="Arcam FMJ Receiver", - media_class=MEDIA_CLASS_DIRECTORY, + media_class=MediaClass.DIRECTORY, media_content_id="root", media_content_type="library", can_play=False, @@ -237,7 +237,7 @@ class ArcamFmj(MediaPlayerEntity): return root 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 media.""" @@ -289,13 +289,13 @@ class ArcamFmj(MediaPlayerEntity): return value / 99.0 @property - def media_content_type(self): + def media_content_type(self) -> MediaType | None: """Content type of current playing media.""" source = self._state.get_source() if source == SourceCodes.DAB: - value = MEDIA_TYPE_MUSIC + value = MediaType.MUSIC elif source == SourceCodes.FM: - value = MEDIA_TYPE_MUSIC + value = MediaType.MUSIC else: value = None return value diff --git a/homeassistant/components/arris_tg2492lg/device_tracker.py b/homeassistant/components/arris_tg2492lg/device_tracker.py index 5bc157cbb6b..b456aa3f703 100644 --- a/homeassistant/components/arris_tg2492lg/device_tracker.py +++ b/homeassistant/components/arris_tg2492lg/device_tracker.py @@ -24,7 +24,7 @@ PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( ) -def get_scanner(hass: HomeAssistant, config: ConfigType) -> DeviceScanner: +def get_scanner(hass: HomeAssistant, config: ConfigType) -> ArrisDeviceScanner: """Return the Arris device scanner.""" conf = config[DOMAIN] url = f"http://{conf[CONF_HOST]}" diff --git a/homeassistant/components/aruba/device_tracker.py b/homeassistant/components/aruba/device_tracker.py index dc2d2fee8e9..d0794553b42 100644 --- a/homeassistant/components/aruba/device_tracker.py +++ b/homeassistant/components/aruba/device_tracker.py @@ -34,7 +34,7 @@ PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( ) -def get_scanner(hass: HomeAssistant, config: ConfigType) -> DeviceScanner | None: +def get_scanner(hass: HomeAssistant, config: ConfigType) -> ArubaDeviceScanner | None: """Validate the configuration and return a Aruba scanner.""" scanner = ArubaDeviceScanner(config[DOMAIN]) diff --git a/homeassistant/components/aseko_pool_live/translations/pt.json b/homeassistant/components/aseko_pool_live/translations/pt.json index 2933743c867..cdb482efaa8 100644 --- a/homeassistant/components/aseko_pool_live/translations/pt.json +++ b/homeassistant/components/aseko_pool_live/translations/pt.json @@ -1,5 +1,13 @@ { "config": { + "abort": { + "already_configured": "Conta j\u00e1 configurada" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/asterisk_mbox/mailbox.py b/homeassistant/components/asterisk_mbox/mailbox.py index 04d2be70704..edf95cb3787 100644 --- a/homeassistant/components/asterisk_mbox/mailbox.py +++ b/homeassistant/components/asterisk_mbox/mailbox.py @@ -3,6 +3,7 @@ from __future__ import annotations from functools import partial import logging +from typing import Any from asterisk_mbox import ServerError @@ -11,7 +12,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import DOMAIN as ASTERISK_DOMAIN +from . import DOMAIN as ASTERISK_DOMAIN, AsteriskData _LOGGER = logging.getLogger(__name__) @@ -31,7 +32,7 @@ async def async_get_handler( class AsteriskMailbox(Mailbox): """Asterisk VM Sensor.""" - def __init__(self, hass, name): + def __init__(self, hass: HomeAssistant, name: str) -> None: """Initialize Asterisk mailbox.""" super().__init__(hass, name) async_dispatcher_connect( @@ -39,29 +40,30 @@ class AsteriskMailbox(Mailbox): ) @callback - def _update_callback(self, msg): + def _update_callback(self, msg: str) -> None: """Update the message count in HA, if needed.""" self.async_update() @property - def media_type(self): + def media_type(self) -> str: """Return the supported media type.""" return CONTENT_TYPE_MPEG @property - def can_delete(self): + def can_delete(self) -> bool: """Return if messages can be deleted.""" return True @property - def has_media(self): + def has_media(self) -> bool: """Return if messages have attached media files.""" return True - async def async_get_media(self, msgid): + async def async_get_media(self, msgid: str) -> bytes: """Return the media blob for the msgid.""" - client = self.hass.data[ASTERISK_DOMAIN].client + data: AsteriskData = self.hass.data[ASTERISK_DOMAIN] + client = data.client try: return await self.hass.async_add_executor_job( partial(client.mp3, msgid, sync=True) @@ -69,13 +71,15 @@ class AsteriskMailbox(Mailbox): except ServerError as err: raise StreamError(err) from err - async def async_get_messages(self): + async def async_get_messages(self) -> list[dict[str, Any]]: """Return a list of the current messages.""" - return self.hass.data[ASTERISK_DOMAIN].messages + data: AsteriskData = self.hass.data[ASTERISK_DOMAIN] + return data.messages - async def async_delete(self, msgid): + async def async_delete(self, msgid: str) -> bool: """Delete the specified messages.""" - client = self.hass.data[ASTERISK_DOMAIN].client + data: AsteriskData = self.hass.data[ASTERISK_DOMAIN] + client = data.client _LOGGER.info("Deleting: %s", msgid) await self.hass.async_add_executor_job(client.delete, msgid) return True diff --git a/homeassistant/components/asuswrt/config_flow.py b/homeassistant/components/asuswrt/config_flow.py index ec5cccb9a71..94843a4c07c 100644 --- a/homeassistant/components/asuswrt/config_flow.py +++ b/homeassistant/components/asuswrt/config_flow.py @@ -9,7 +9,7 @@ from typing import Any import voluptuous as vol -from homeassistant.components.device_tracker.const import ( +from homeassistant.components.device_tracker import ( CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME, ) diff --git a/homeassistant/components/asuswrt/router.py b/homeassistant/components/asuswrt/router.py index f68c77a2a66..a48e1374b6d 100644 --- a/homeassistant/components/asuswrt/router.py +++ b/homeassistant/components/asuswrt/router.py @@ -8,7 +8,7 @@ from typing import Any from aioasuswrt.asuswrt import AsusWrt, Device as WrtDevice -from homeassistant.components.device_tracker.const import ( +from homeassistant.components.device_tracker import ( CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME, DOMAIN as TRACKER_DOMAIN, diff --git a/homeassistant/components/asuswrt/translations/ja.json b/homeassistant/components/asuswrt/translations/ja.json index 6bbe87fbf7e..411d7ac68a5 100644 --- a/homeassistant/components/asuswrt/translations/ja.json +++ b/homeassistant/components/asuswrt/translations/ja.json @@ -19,7 +19,7 @@ "mode": "\u30e2\u30fc\u30c9", "name": "\u540d\u524d", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", - "port": "\u30dd\u30fc\u30c8", + "port": "\u30dd\u30fc\u30c8 (\u30d7\u30ed\u30c8\u30b3\u30eb\u306e\u30c7\u30d5\u30a9\u30eb\u30c8\u306e\u5834\u5408\u306f\u7a7a\u306e\u307e\u307e\u306b\u3057\u3066\u304a\u304d\u307e\u3059)", "protocol": "\u4f7f\u7528\u3059\u308b\u901a\u4fe1\u30d7\u30ed\u30c8\u30b3\u30eb", "ssh_key": "SSH\u30ad\u30fc \u30d5\u30a1\u30a4\u30eb\u3078\u306e\u30d1\u30b9 (\u30d1\u30b9\u30ef\u30fc\u30c9\u306e\u4ee3\u308f\u308a)", "username": "\u30e6\u30fc\u30b6\u30fc\u540d" @@ -37,7 +37,7 @@ "dnsmasq": "dnsmasq.leases\u30d5\u30a1\u30a4\u30eb\u306e\u30eb\u30fc\u30bf\u30fc\u5185\u306e\u5834\u6240", "interface": "\u7d71\u8a08\u3092\u53d6\u5f97\u3057\u305f\u3044\u30a4\u30f3\u30bf\u30d5\u30a7\u30fc\u30b9(\u4f8b: eth0\u3001eth1\u306a\u3069)", "require_ip": "\u30c7\u30d0\u30a4\u30b9\u306b\u306fIP\u304c\u5fc5\u8981\u3067\u3059(\u30a2\u30af\u30bb\u30b9\u30dd\u30a4\u30f3\u30c8\u30e2\u30fc\u30c9\u306e\u5834\u5408)", - "track_unknown": "\u8ffd\u8de1\u4e0d\u660e/\u540d\u524d\u306e\u306a\u3044\u30c7\u30d0\u30a4\u30b9" + "track_unknown": "\u4e0d\u660e/\u540d\u524d\u306e\u306a\u3044\u30c7\u30d0\u30a4\u30b9\u3092\u8ffd\u8de1\u3059\u308b" }, "title": "AsusWRT\u306e\u30aa\u30d7\u30b7\u30e7\u30f3" } diff --git a/homeassistant/components/atag/climate.py b/homeassistant/components/atag/climate.py index e6e9d8503e8..ce52bd4fd65 100644 --- a/homeassistant/components/atag/climate.py +++ b/homeassistant/components/atag/climate.py @@ -3,10 +3,10 @@ from __future__ import annotations from typing import Any -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( PRESET_AWAY, PRESET_BOOST, + ClimateEntity, ClimateEntityFeature, HVACAction, HVACMode, diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index 3aef3f5960d..a816ddc06ff 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.2"], + "requirements": ["yalexs==1.2.4"], "codeowners": ["@bdraco"], "dhcp": [ { diff --git a/homeassistant/components/august/translations/es.json b/homeassistant/components/august/translations/es.json index b9334d7b473..25f62e6c1db 100644 --- a/homeassistant/components/august/translations/es.json +++ b/homeassistant/components/august/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "error": { "cannot_connect": "No se pudo conectar", diff --git a/homeassistant/components/aurora_abb_powerone/translations/cs.json b/homeassistant/components/aurora_abb_powerone/translations/cs.json new file mode 100644 index 00000000000..33006d6761b --- /dev/null +++ b/homeassistant/components/aurora_abb_powerone/translations/cs.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aussie_broadband/translations/cs.json b/homeassistant/components/aussie_broadband/translations/cs.json index 131dddca93a..7f734257985 100644 --- a/homeassistant/components/aussie_broadband/translations/cs.json +++ b/homeassistant/components/aussie_broadband/translations/cs.json @@ -10,6 +10,12 @@ "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, "step": { + "reauth_confirm": { + "data": { + "password": "Heslo" + }, + "title": "Znovu ov\u011b\u0159it integraci" + }, "user": { "data": { "password": "Heslo", diff --git a/homeassistant/components/aussie_broadband/translations/es.json b/homeassistant/components/aussie_broadband/translations/es.json index ce0f6022094..49165aaf40f 100644 --- a/homeassistant/components/aussie_broadband/translations/es.json +++ b/homeassistant/components/aussie_broadband/translations/es.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", "no_services_found": "No se encontraron servicios para esta cuenta", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "error": { "cannot_connect": "No se pudo conectar", diff --git a/homeassistant/components/aussie_broadband/translations/pt.json b/homeassistant/components/aussie_broadband/translations/pt.json index 5a8312a5ac1..535612a4dbd 100644 --- a/homeassistant/components/aussie_broadband/translations/pt.json +++ b/homeassistant/components/aussie_broadband/translations/pt.json @@ -1,6 +1,10 @@ { "config": { + "abort": { + "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida" + }, "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "unknown": "Erro inesperado" }, @@ -8,10 +12,12 @@ "reauth_confirm": { "data": { "password": "Palavra-passe" - } + }, + "title": "Reautenticar integra\u00e7\u00e3o" }, "user": { "data": { + "password": "Palavra-passe", "username": "Nome de Utilizador" } } @@ -19,6 +25,8 @@ }, "options": { "abort": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "unknown": "Erro inesperado" } } diff --git a/homeassistant/components/auth/manifest.json b/homeassistant/components/auth/manifest.json index 2674bdfb032..200e41713d6 100644 --- a/homeassistant/components/auth/manifest.json +++ b/homeassistant/components/auth/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/auth", "dependencies": ["http"], "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "system" } diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 685edc3b6d7..3037d7cc3a7 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -1,14 +1,14 @@ """Allow to set up simple automation rules via the config file.""" from __future__ import annotations -from collections.abc import Callable +from collections.abc import Callable, Mapping import logging -from typing import Any, cast +from typing import Any, Protocol, cast import voluptuous as vol from voluptuous.humanize import humanize_error -from homeassistant.components import blueprint +from homeassistant.components import blueprint, websocket_api from homeassistant.components.blueprint import CONF_USE_BLUEPRINT from homeassistant.const import ( ATTR_ENTITY_ID, @@ -33,9 +33,12 @@ from homeassistant.const import ( STATE_ON, ) from homeassistant.core import ( + CALLBACK_TYPE, Context, CoreState, + Event, HomeAssistant, + ServiceCall, callback, split_entity_id, valid_entity_id, @@ -101,9 +104,6 @@ from .const import ( from .helpers import async_get_blueprints from .trace import trace_automation -# mypy: allow-untyped-calls, allow-untyped-defs -# mypy: no-check-untyped-defs, no-warn-return-any - ENTITY_ID_FORMAT = DOMAIN + ".{}" @@ -122,6 +122,15 @@ SERVICE_TRIGGER = "trigger" _LOGGER = logging.getLogger(__name__) +class IfAction(Protocol): + """Define the format of if_action.""" + + config: list[ConfigType] + + def __call__(self, variables: Mapping[str, Any] | None = None) -> bool: + """AND all conditions.""" + + # AutomationActionType, AutomationTriggerData, # and AutomationTriggerInfo are deprecated as of 2022.9. AutomationActionType = TriggerActionType @@ -130,7 +139,7 @@ AutomationTriggerInfo = TriggerInfo @bind_hass -def is_on(hass, entity_id): +def is_on(hass: HomeAssistant, entity_id: str) -> bool: """ Return true if specified automation entity_id is on. @@ -139,91 +148,71 @@ def is_on(hass, entity_id): return hass.states.is_state(entity_id, STATE_ON) -@callback -def automations_with_entity(hass: HomeAssistant, entity_id: str) -> list[str]: - """Return all automations that reference the entity.""" +def _automations_with_x( + hass: HomeAssistant, referenced_id: str, property_name: str +) -> list[str]: + """Return all automations that reference the x.""" if DOMAIN not in hass.data: return [] - component = hass.data[DOMAIN] + component: EntityComponent[AutomationEntity] = hass.data[DOMAIN] return [ automation_entity.entity_id for automation_entity in component.entities - if entity_id in automation_entity.referenced_entities + if referenced_id in getattr(automation_entity, property_name) ] -@callback -def entities_in_automation(hass: HomeAssistant, entity_id: str) -> list[str]: - """Return all entities in a scene.""" +def _x_in_automation( + hass: HomeAssistant, entity_id: str, property_name: str +) -> list[str]: + """Return all x in an automation.""" if DOMAIN not in hass.data: return [] - component = hass.data[DOMAIN] + component: EntityComponent[AutomationEntity] = hass.data[DOMAIN] if (automation_entity := component.get_entity(entity_id)) is None: return [] - return list(automation_entity.referenced_entities) + return list(getattr(automation_entity, property_name)) + + +@callback +def automations_with_entity(hass: HomeAssistant, entity_id: str) -> list[str]: + """Return all automations that reference the entity.""" + return _automations_with_x(hass, entity_id, "referenced_entities") + + +@callback +def entities_in_automation(hass: HomeAssistant, entity_id: str) -> list[str]: + """Return all entities in an automation.""" + return _x_in_automation(hass, entity_id, "referenced_entities") @callback def automations_with_device(hass: HomeAssistant, device_id: str) -> list[str]: """Return all automations that reference the device.""" - if DOMAIN not in hass.data: - return [] - - component = hass.data[DOMAIN] - - return [ - automation_entity.entity_id - for automation_entity in component.entities - if device_id in automation_entity.referenced_devices - ] + return _automations_with_x(hass, device_id, "referenced_devices") @callback def devices_in_automation(hass: HomeAssistant, entity_id: str) -> list[str]: - """Return all devices in a scene.""" - if DOMAIN not in hass.data: - return [] - - component = hass.data[DOMAIN] - - if (automation_entity := component.get_entity(entity_id)) is None: - return [] - - return list(automation_entity.referenced_devices) + """Return all devices in an automation.""" + return _x_in_automation(hass, entity_id, "referenced_devices") @callback def automations_with_area(hass: HomeAssistant, area_id: str) -> list[str]: """Return all automations that reference the area.""" - if DOMAIN not in hass.data: - return [] - - component = hass.data[DOMAIN] - - return [ - automation_entity.entity_id - for automation_entity in component.entities - if area_id in automation_entity.referenced_areas - ] + return _automations_with_x(hass, area_id, "referenced_areas") @callback def areas_in_automation(hass: HomeAssistant, entity_id: str) -> list[str]: """Return all areas in an automation.""" - if DOMAIN not in hass.data: - return [] - - component = hass.data[DOMAIN] - - if (automation_entity := component.get_entity(entity_id)) is None: - return [] - - return list(automation_entity.referenced_areas) + return _x_in_automation(hass, entity_id, "referenced_areas") @callback @@ -232,7 +221,7 @@ def automations_with_blueprint(hass: HomeAssistant, blueprint_path: str) -> list if DOMAIN not in hass.data: return [] - component = hass.data[DOMAIN] + component: EntityComponent[AutomationEntity] = hass.data[DOMAIN] return [ automation_entity.entity_id @@ -243,7 +232,9 @@ def automations_with_blueprint(hass: HomeAssistant, blueprint_path: str) -> list async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up all automations.""" - hass.data[DOMAIN] = component = EntityComponent(LOGGER, DOMAIN, hass) + hass.data[DOMAIN] = component = EntityComponent[AutomationEntity]( + LOGGER, DOMAIN, hass + ) # Process integration platforms right away since # we will create entities before firing EVENT_COMPONENT_LOADED @@ -255,7 +246,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: if not await _async_process_config(hass, config, component): await async_get_blueprints(hass).async_populate() - async def trigger_service_handler(entity, service_call): + async def trigger_service_handler( + entity: AutomationEntity, service_call: ServiceCall + ) -> None: """Handle forced automation trigger, e.g. from frontend.""" await entity.async_trigger( {**service_call.data[ATTR_VARIABLES], "trigger": {"platform": None}}, @@ -279,7 +272,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: "async_turn_off", ) - async def reload_service_handler(service_call): + async def reload_service_handler(service_call: ServiceCall) -> None: """Remove all automations and load new ones from config.""" if (conf := await component.async_prepare_reload()) is None: return @@ -297,6 +290,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: schema=vol.Schema({}), ) + websocket_api.async_register_command(hass, websocket_config) + return True @@ -307,22 +302,22 @@ class AutomationEntity(ToggleEntity, RestoreEntity): def __init__( self, - automation_id, - name, - trigger_config, - cond_func, - action_script, - initial_state, - variables, - trigger_variables, - raw_config, - blueprint_inputs, - trace_config, - ): + automation_id: str | None, + name: str, + trigger_config: list[ConfigType], + cond_func: IfAction | None, + action_script: Script, + initial_state: bool | None, + variables: ScriptVariables | None, + trigger_variables: ScriptVariables | None, + raw_config: ConfigType | None, + blueprint_inputs: ConfigType | None, + trace_config: ConfigType, + ) -> None: """Initialize an automation entity.""" self._attr_name = name self._trigger_config = trigger_config - self._async_detach_triggers = None + self._async_detach_triggers: CALLBACK_TYPE | None = None self._cond_func = cond_func self.action_script = action_script self.action_script.change_listener = self.async_write_ha_state @@ -331,15 +326,15 @@ class AutomationEntity(ToggleEntity, RestoreEntity): self._referenced_entities: set[str] | None = None self._referenced_devices: set[str] | None = None self._logger = LOGGER - self._variables: ScriptVariables = variables - self._trigger_variables: ScriptVariables = trigger_variables - self._raw_config = raw_config + self._variables = variables + self._trigger_variables = trigger_variables + self.raw_config = raw_config self._blueprint_inputs = blueprint_inputs self._trace_config = trace_config self._attr_unique_id = automation_id @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the entity state attributes.""" attrs = { ATTR_LAST_TRIGGERED: self.action_script.last_triggered, @@ -358,7 +353,7 @@ class AutomationEntity(ToggleEntity, RestoreEntity): return self._async_detach_triggers is not None or self._is_enabled @property - def referenced_areas(self): + def referenced_areas(self) -> set[str]: """Return a set of referenced areas.""" return self.action_script.referenced_areas @@ -388,7 +383,7 @@ class AutomationEntity(ToggleEntity, RestoreEntity): return referenced @property - def referenced_entities(self): + def referenced_entities(self) -> set[str]: """Return a set of referenced entities.""" if self._referenced_entities is not None: return self._referenced_entities @@ -484,7 +479,7 @@ class AutomationEntity(ToggleEntity, RestoreEntity): with trace_automation( self.hass, self.unique_id, - self._raw_config, + self.raw_config, self._blueprint_inputs, trigger_context, self._trace_config, @@ -537,7 +532,7 @@ class AutomationEntity(ToggleEntity, RestoreEntity): event_data[ATTR_SOURCE] = variables["trigger"]["description"] @callback - def started_action(): + def started_action() -> None: self.hass.bus.async_fire( EVENT_AUTOMATION_TRIGGERED, event_data, context=trigger_context ) @@ -579,12 +574,12 @@ class AutomationEntity(ToggleEntity, RestoreEntity): self._logger.exception("While executing automation %s", self.entity_id) automation_trace.set_error(err) - async def async_will_remove_from_hass(self): + async def async_will_remove_from_hass(self) -> None: """Remove listeners when removing automation from Home Assistant.""" await super().async_will_remove_from_hass() await self.async_disable() - async def async_enable(self): + async def async_enable(self) -> None: """Enable this automation entity. This method is a coroutine. @@ -600,7 +595,7 @@ class AutomationEntity(ToggleEntity, RestoreEntity): self.async_write_ha_state() return - async def async_enable_automation(event): + async def async_enable_automation(event: Event) -> None: """Start automation on startup.""" # Don't do anything if no longer enabled or already attached if not self._is_enabled or self._async_detach_triggers is not None: @@ -613,7 +608,7 @@ class AutomationEntity(ToggleEntity, RestoreEntity): ) self.async_write_ha_state() - async def async_disable(self, stop_actions=DEFAULT_STOP_ACTIONS): + async def async_disable(self, stop_actions: bool = DEFAULT_STOP_ACTIONS) -> None: """Disable the automation entity.""" if not self._is_enabled and not self.action_script.runs: return @@ -634,7 +629,7 @@ class AutomationEntity(ToggleEntity, RestoreEntity): ) -> Callable[[], None] | None: """Set up the triggers.""" - def log_cb(level, msg, **kwargs): + def log_cb(level: int, msg: str, **kwargs: Any) -> None: self._logger.log(level, "%s %s", msg, self.name, **kwargs) this = None @@ -668,13 +663,13 @@ class AutomationEntity(ToggleEntity, RestoreEntity): async def _async_process_config( hass: HomeAssistant, config: dict[str, Any], - component: EntityComponent, + component: EntityComponent[AutomationEntity], ) -> bool: """Process config and add automations. Returns if blueprints were used. """ - entities = [] + entities: list[AutomationEntity] = [] blueprints_used = False for config_key in extract_domain_configs(config, DOMAIN): @@ -705,10 +700,10 @@ async def _async_process_config( else: raw_config = cast(AutomationConfig, config_block).raw_config - automation_id = config_block.get(CONF_ID) - name = config_block.get(CONF_ALIAS) or f"{config_key} {list_no}" + automation_id: str | None = config_block.get(CONF_ID) + name: str = config_block.get(CONF_ALIAS) or f"{config_key} {list_no}" - initial_state = config_block.get(CONF_INITIAL_STATE) + initial_state: bool | None = config_block.get(CONF_INITIAL_STATE) action_script = Script( hass, @@ -767,11 +762,13 @@ async def _async_process_config( return blueprints_used -async def _async_process_if(hass, name, config, p_config): +async def _async_process_if( + hass: HomeAssistant, name: str, config: dict[str, Any], p_config: dict[str, Any] +) -> IfAction | None: """Process if checks.""" if_configs = p_config[CONF_CONDITION] - checks = [] + checks: list[condition.ConditionCheckerType] = [] for if_config in if_configs: try: checks.append(await condition.async_from_config(hass, if_config)) @@ -779,9 +776,9 @@ async def _async_process_if(hass, name, config, p_config): LOGGER.warning("Invalid condition: %s", ex) return None - def if_action(variables=None): + def if_action(variables: Mapping[str, Any] | None = None) -> bool: """AND all conditions.""" - errors = [] + errors: list[ConditionErrorIndex] = [] for index, check in enumerate(checks): try: with trace_path(["condition", str(index)]): @@ -804,9 +801,10 @@ async def _async_process_if(hass, name, config, p_config): return True - if_action.config = if_configs + result: IfAction = if_action # type: ignore[assignment] + result.config = if_configs - return if_action + return result @callback @@ -824,7 +822,7 @@ def _trigger_extract_devices(trigger_conf: dict) -> list[str]: return [trigger_conf[CONF_EVENT_DATA][CONF_DEVICE_ID]] if trigger_conf[CONF_PLATFORM] == "tag" and CONF_DEVICE_ID in trigger_conf: - return trigger_conf[CONF_DEVICE_ID] + return trigger_conf[CONF_DEVICE_ID] # type: ignore[no-any-return] return [] @@ -833,13 +831,13 @@ def _trigger_extract_devices(trigger_conf: dict) -> list[str]: def _trigger_extract_entities(trigger_conf: dict) -> list[str]: """Extract entities from a trigger config.""" if trigger_conf[CONF_PLATFORM] in ("state", "numeric_state"): - return trigger_conf[CONF_ENTITY_ID] + return trigger_conf[CONF_ENTITY_ID] # type: ignore[no-any-return] if trigger_conf[CONF_PLATFORM] == "calendar": return [trigger_conf[CONF_ENTITY_ID]] if trigger_conf[CONF_PLATFORM] == "zone": - return trigger_conf[CONF_ENTITY_ID] + [trigger_conf[CONF_ZONE]] + return trigger_conf[CONF_ENTITY_ID] + [trigger_conf[CONF_ZONE]] # type: ignore[no-any-return] if trigger_conf[CONF_PLATFORM] == "geo_location": return [trigger_conf[CONF_ZONE]] @@ -857,3 +855,28 @@ def _trigger_extract_entities(trigger_conf: dict) -> list[str]: return [trigger_conf[CONF_EVENT_DATA][CONF_ENTITY_ID]] return [] + + +@websocket_api.websocket_command({"type": "automation/config", "entity_id": str}) +def websocket_config( + hass: HomeAssistant, + connection: websocket_api.ActiveConnection, + msg: dict[str, Any], +) -> None: + """Get automation config.""" + component: EntityComponent[AutomationEntity] = hass.data[DOMAIN] + + automation = component.get_entity(msg["entity_id"]) + + if automation is None: + connection.send_error( + msg["id"], websocket_api.const.ERR_NOT_FOUND, "Entity not found" + ) + return + + connection.send_result( + msg["id"], + { + "config": automation.raw_config, + }, + ) diff --git a/homeassistant/components/automation/config.py b/homeassistant/components/automation/config.py index 228e78ac446..ec35e617b07 100644 --- a/homeassistant/components/automation/config.py +++ b/homeassistant/components/automation/config.py @@ -1,6 +1,9 @@ """Config validation helper for the automation integration.""" +from __future__ import annotations + import asyncio from contextlib import suppress +from typing import Any import voluptuous as vol @@ -17,10 +20,12 @@ from homeassistant.const import ( CONF_ID, CONF_VARIABLES, ) +from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_per_platform, config_validation as cv, script from homeassistant.helpers.condition import async_validate_conditions_config from homeassistant.helpers.trigger import async_validate_trigger_config +from homeassistant.helpers.typing import ConfigType from homeassistant.loader import IntegrationNotFound from .const import ( @@ -34,9 +39,6 @@ from .const import ( ) from .helpers import async_get_blueprints -# mypy: allow-untyped-calls, allow-untyped-defs -# mypy: no-check-untyped-defs, no-warn-return-any - PACKAGE_MERGE_HINT = "list" _CONDITION_SCHEMA = vol.All(cv.ensure_list, [cv.CONDITION_SCHEMA]) @@ -63,7 +65,11 @@ PLATFORM_SCHEMA = vol.All( ) -async def async_validate_config_item(hass, config, full_config=None): +async def async_validate_config_item( + hass: HomeAssistant, + config: ConfigType, + full_config: ConfigType | None = None, +) -> blueprint.BlueprintInputs | dict[str, Any]: """Validate config item.""" if blueprint.is_blueprint_instance_config(config): blueprints = async_get_blueprints(hass) @@ -90,17 +96,21 @@ async def async_validate_config_item(hass, config, full_config=None): class AutomationConfig(dict): """Dummy class to allow adding attributes.""" - raw_config = None + raw_config: dict[str, Any] | None = None -async def _try_async_validate_config_item(hass, config, full_config=None): +async def _try_async_validate_config_item( + hass: HomeAssistant, + config: dict[str, Any], + full_config: dict[str, Any] | None = None, +) -> AutomationConfig | blueprint.BlueprintInputs | None: """Validate config item.""" raw_config = None with suppress(ValueError): raw_config = dict(config) try: - config = await async_validate_config_item(hass, config, full_config) + validated_config = await async_validate_config_item(hass, config, full_config) except ( vol.Invalid, HomeAssistantError, @@ -110,15 +120,15 @@ async def _try_async_validate_config_item(hass, config, full_config=None): async_log_exception(ex, DOMAIN, full_config or config, hass) return None - if isinstance(config, blueprint.BlueprintInputs): - return config + if isinstance(validated_config, blueprint.BlueprintInputs): + return validated_config - config = AutomationConfig(config) - config.raw_config = raw_config - return config + automation_config = AutomationConfig(validated_config) + automation_config.raw_config = raw_config + return automation_config -async def async_validate_config(hass, config): +async def async_validate_config(hass: HomeAssistant, config: ConfigType) -> ConfigType: """Validate config.""" automations = list( filter( diff --git a/homeassistant/components/automation/logbook.py b/homeassistant/components/automation/logbook.py index 529fed80d26..e5ab351b0be 100644 --- a/homeassistant/components/automation/logbook.py +++ b/homeassistant/components/automation/logbook.py @@ -1,11 +1,11 @@ """Describe logbook events.""" -from homeassistant.components.logbook import LazyEventPartialState -from homeassistant.components.logbook.const import ( +from homeassistant.components.logbook import ( LOGBOOK_ENTRY_CONTEXT_ID, LOGBOOK_ENTRY_ENTITY_ID, LOGBOOK_ENTRY_MESSAGE, LOGBOOK_ENTRY_NAME, LOGBOOK_ENTRY_SOURCE, + LazyEventPartialState, ) from homeassistant.const import ATTR_ENTITY_ID, ATTR_NAME from homeassistant.core import HomeAssistant, callback diff --git a/homeassistant/components/automation/manifest.json b/homeassistant/components/automation/manifest.json index 9dd0130ee2f..3bfb192759c 100644 --- a/homeassistant/components/automation/manifest.json +++ b/homeassistant/components/automation/manifest.json @@ -5,5 +5,6 @@ "dependencies": ["blueprint", "trace"], "after_dependencies": ["device_automation", "webhook"], "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "system" } diff --git a/homeassistant/components/automation/trace.py b/homeassistant/components/automation/trace.py index f76dd57e4ed..ae0d0339bfa 100644 --- a/homeassistant/components/automation/trace.py +++ b/homeassistant/components/automation/trace.py @@ -1,18 +1,20 @@ """Trace support for automation.""" from __future__ import annotations +from collections.abc import Generator from contextlib import contextmanager from typing import Any -from homeassistant.components.trace import ActionTrace, async_store_trace -from homeassistant.components.trace.const import CONF_STORED_TRACES -from homeassistant.core import Context +from homeassistant.components.trace import ( + CONF_STORED_TRACES, + ActionTrace, + async_store_trace, +) +from homeassistant.core import Context, HomeAssistant +from homeassistant.helpers.typing import ConfigType from .const import DOMAIN -# mypy: allow-untyped-calls, allow-untyped-defs -# mypy: no-check-untyped-defs, no-warn-return-any - class AutomationTrace(ActionTrace): """Container for automation trace.""" @@ -21,9 +23,9 @@ class AutomationTrace(ActionTrace): def __init__( self, - item_id: str, - config: dict[str, Any], - blueprint_inputs: dict[str, Any], + item_id: str | None, + config: ConfigType | None, + blueprint_inputs: ConfigType | None, context: Context, ) -> None: """Container for automation trace.""" @@ -46,8 +48,13 @@ class AutomationTrace(ActionTrace): @contextmanager def trace_automation( - hass, automation_id, config, blueprint_inputs, context, trace_config -): + hass: HomeAssistant, + automation_id: str | None, + config: ConfigType | None, + blueprint_inputs: ConfigType | None, + context: Context, + trace_config: ConfigType, +) -> Generator[AutomationTrace, None, None]: """Trace action execution of automation with automation_id.""" trace = AutomationTrace(automation_id, config, blueprint_inputs, context) async_store_trace(hass, trace, trace_config[CONF_STORED_TRACES]) diff --git a/homeassistant/components/automation/translations/bg.json b/homeassistant/components/automation/translations/bg.json index 1e294bff9a7..2765eab3ce3 100644 --- a/homeassistant/components/automation/translations/bg.json +++ b/homeassistant/components/automation/translations/bg.json @@ -1,4 +1,16 @@ { + "issues": { + "service_not_found": { + "fix_flow": { + "step": { + "confirm": { + "title": "{name} \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430 \u043d\u0435\u043f\u043e\u0437\u043d\u0430\u0442\u0430 \u0443\u0441\u043b\u0443\u0433\u0430" + } + } + }, + "title": "{name} \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430 \u043d\u0435\u043f\u043e\u0437\u043d\u0430\u0442\u0430 \u0443\u0441\u043b\u0443\u0433\u0430" + } + }, "state": { "_": { "off": "\u0418\u0437\u043a\u043b\u044e\u0447\u0435\u043d", diff --git a/homeassistant/components/automation/translations/ca.json b/homeassistant/components/automation/translations/ca.json index 4a6cc33e04c..3a85ab5fe22 100644 --- a/homeassistant/components/automation/translations/ca.json +++ b/homeassistant/components/automation/translations/ca.json @@ -4,6 +4,7 @@ "fix_flow": { "step": { "confirm": { + "description": "L'automatitzaci\u00f3 \"{name}\" (`{entity_id}`) cont\u00e9 una acci\u00f3 que crida el servei desconegut: `{service}`.\n\nAix\u00f2 fa que l'automatitzaci\u00f3 no funcioni correctament. Potser aquest servei ja no est\u00e0 disponible, o potser un l'ha causat un error ortogr\u00e0fic o d'escriptura. \n\nPer corregir aquest error, [edita l'automatitzaci\u00f3]({edit}) i elimina l'acci\u00f3 que crida aquest servei.\n\nFes clic a ENVIA, a continuaci\u00f3, quan hagis solucionat l'error d'aquesta automatitzaci\u00f3.", "title": "{name} utilitza un servei desconegut" } } diff --git a/homeassistant/components/automation/translations/cs.json b/homeassistant/components/automation/translations/cs.json index b4b3c61be5a..a59030fd397 100644 --- a/homeassistant/components/automation/translations/cs.json +++ b/homeassistant/components/automation/translations/cs.json @@ -1,4 +1,17 @@ { + "issues": { + "service_not_found": { + "fix_flow": { + "step": { + "confirm": { + "description": "Automatizace \"{name}\" (`{entity_id}`) m\u00e1 akci, kter\u00e1 vol\u00e1 nezn\u00e1mou slu\u017ebu: `{service}`.\n\nTato chyba br\u00e1n\u00ed spr\u00e1vn\u00e9mu spu\u0161t\u011bn\u00ed automatizace. Mo\u017en\u00e1 tato slu\u017eba ji\u017e nen\u00ed k dispozici, nebo ji zp\u016fsobil p\u0159eklep.\n\nChcete-li tuto chybu opravit, [upravte automatizaci]({edit}) a odstra\u0148te akci, kter\u00e1 tuto slu\u017ebu vol\u00e1.\n\nKliknut\u00edm na tla\u010d\u00edtko ULO\u017dIT potvr\u010fte, \u017ee jste tuto automatizaci opravili.", + "title": "{name} pou\u017e\u00edv\u00e1 nezn\u00e1mou slu\u017ebu" + } + } + }, + "title": "{name} pou\u017e\u00edv\u00e1 nezn\u00e1mou slu\u017ebu" + } + }, "state": { "_": { "off": "Vypnuto", diff --git a/homeassistant/components/automation/translations/id.json b/homeassistant/components/automation/translations/id.json index acf1dfab41b..8bee43fadec 100644 --- a/homeassistant/components/automation/translations/id.json +++ b/homeassistant/components/automation/translations/id.json @@ -4,7 +4,7 @@ "fix_flow": { "step": { "confirm": { - "description": "Otomasi \"{nama}\" (`{entity_id}`) memiliki aksi yang memanggil layanan yang tidak diketahui: `{service}`.\n\nKesalahan ini mencegah otomasi berjalan dengan benar. Mungkin layanan ini tidak lagi tersedia, atau mungkin kesalahan ketik yang menyebabkannya.\n\nUntuk memperbaiki kesalahan ini, [edit otomasi]({edit}) dan hapus aksi yang memanggil layanan ini.\n\nKlik KIRIM di bawah ini untuk mengonfirmasi bahwa Anda telah memperbaiki otomasi ini.", + "description": "Otomasi \"{name}\" (`{entity_id}`) memiliki aksi yang memanggil layanan yang tidak diketahui: `{service}`.\n\nKesalahan ini mencegah otomasi berjalan dengan benar. Mungkin layanan ini tidak lagi tersedia, atau mungkin kesalahan ketik yang menyebabkannya.\n\nUntuk memperbaiki kesalahan ini, [edit otomasi]({edit}) dan hapus aksi yang memanggil layanan ini.\n\nKlik KIRIM di bawah ini untuk mengonfirmasi bahwa Anda telah memperbaiki otomasi ini.", "title": "{name} menggunakan layanan yang tidak dikenal" } } diff --git a/homeassistant/components/automation/translations/ja.json b/homeassistant/components/automation/translations/ja.json index 4392cebdbd0..150ef345e68 100644 --- a/homeassistant/components/automation/translations/ja.json +++ b/homeassistant/components/automation/translations/ja.json @@ -4,6 +4,7 @@ "fix_flow": { "step": { "confirm": { + "description": "\u81ea\u52d5\u5316 \" {name} \" (` {entity_id} `) \u306b\u306f\u3001\u4e0d\u660e\u306a\u30b5\u30fc\u30d3\u30b9 ` {service} ` \u3092\u547c\u3073\u51fa\u3059\u30a2\u30af\u30b7\u30e7\u30f3\u304c\u3042\u308a\u307e\u3059\u3002 \n\n\u3053\u306e\u30a8\u30e9\u30fc\u306b\u3088\u308a\u3001\u81ea\u52d5\u5316\u304c\u6b63\u3057\u304f\u5b9f\u884c\u3055\u308c\u306a\u304f\u306a\u308a\u307e\u3059\u3002\u3053\u306e\u30b5\u30fc\u30d3\u30b9\u304c\u5229\u7528\u3067\u304d\u306a\u304f\u306a\u3063\u305f\u304b\u3001\u30bf\u30a4\u30d7\u30df\u30b9\u304c\u539f\u56e0\u3067\u3042\u308b\u53ef\u80fd\u6027\u304c\u3042\u308a\u307e\u3059\u3002 \n\n\u3053\u306e\u30a8\u30e9\u30fc\u3092\u4fee\u6b63\u3059\u308b\u306b\u306f\u3001[\u30aa\u30fc\u30c8\u30e1\u30fc\u30b7\u30e7\u30f3\u3092\u7de8\u96c6]( {edit} ) \u3057\u3001\u3053\u306e\u30b5\u30fc\u30d3\u30b9\u3092\u547c\u3073\u51fa\u3059\u30a2\u30af\u30b7\u30e7\u30f3\u3092\u524a\u9664\u3057\u307e\u3059\u3002 \n\n\u4e0b\u306e\u9001\u4fe1\u3092\u30af\u30ea\u30c3\u30af\u3057\u3066\u3001\u3053\u306e\u81ea\u52d5\u5316\u3092\u4fee\u6b63\u3057\u305f\u3053\u3068\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "title": "{name} \u306f\u3001\u4e0d\u660e\u306a\u30b5\u30fc\u30d3\u30b9\u3092\u4f7f\u7528\u3057\u3066\u3044\u307e\u3059" } } diff --git a/homeassistant/components/automation/translations/pl.json b/homeassistant/components/automation/translations/pl.json index 959fb48b355..81bbbcbc7cd 100644 --- a/homeassistant/components/automation/translations/pl.json +++ b/homeassistant/components/automation/translations/pl.json @@ -1,4 +1,17 @@ { + "issues": { + "service_not_found": { + "fix_flow": { + "step": { + "confirm": { + "description": "Automatyzacja \"{name}\" (`{entity_id}`) posiada akcj\u0119, kt\u00f3ra wywo\u0142uje nieznan\u0105 us\u0142ug\u0119: `{service}`. \n\nTen b\u0142\u0105d uniemo\u017cliwia prawid\u0142owe dzia\u0142anie automatyzacji. Mo\u017ce ta us\u0142uga nie jest ju\u017c dost\u0119pna, a mo\u017ce spowodowa\u0142a to liter\u00f3wka. \n\nAby naprawi\u0107 ten b\u0142\u0105d, [edytuj automatyzacj\u0119]({edit}) i usu\u0144 dzia\u0142anie wywo\u0142uj\u0105ce t\u0119 us\u0142ug\u0119. \n\nKliknij ZATWIERD\u0179 poni\u017cej, aby potwierdzi\u0107, \u017ce naprawi\u0142e\u015b t\u0119 automatyzacj\u0119.", + "title": "{name} korzysta z nieznanej us\u0142ugi" + } + } + }, + "title": "{name} korzysta z nieznanej us\u0142ugi" + } + }, "state": { "_": { "off": "wy\u0142.", diff --git a/homeassistant/components/automation/translations/pt.json b/homeassistant/components/automation/translations/pt.json index 447658433e5..b406550ac45 100644 --- a/homeassistant/components/automation/translations/pt.json +++ b/homeassistant/components/automation/translations/pt.json @@ -1,4 +1,17 @@ { + "issues": { + "service_not_found": { + "fix_flow": { + "step": { + "confirm": { + "description": "A automa\u00e7\u00e3o \" {name} \" (` {entity_id} `) tem uma a\u00e7\u00e3o que chama um servi\u00e7o desconhecido: ` {service} `. \n\n Este erro impede que a automa\u00e7\u00e3o seja executada corretamente. Talvez este servi\u00e7o n\u00e3o esteja mais dispon\u00edvel, ou talvez um erro de digita\u00e7\u00e3o o tenha causado. \n\n Para corrigir esse erro, [edite a automa\u00e7\u00e3o]( {edit} ) e remova a a\u00e7\u00e3o que chama este servi\u00e7o. \n\n Clique em ENVIAR abaixo para confirmar que voc\u00ea corrigiu essa automa\u00e7\u00e3o.", + "title": "{name} usa um servi\u00e7o desconhecido" + } + } + }, + "title": "{name} usa um servi\u00e7o desconhecido" + } + }, "state": { "_": { "off": "Desligado", diff --git a/homeassistant/components/automation/translations/ru.json b/homeassistant/components/automation/translations/ru.json index d98f55a898e..222d67922f8 100644 --- a/homeassistant/components/automation/translations/ru.json +++ b/homeassistant/components/automation/translations/ru.json @@ -1,4 +1,17 @@ { + "issues": { + "service_not_found": { + "fix_flow": { + "step": { + "confirm": { + "description": "\u0412 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0438\u0438 \"{name}\" (`{entity_id}`) \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0435, \u043a\u043e\u0442\u043e\u0440\u043e\u0435 \u0432\u044b\u0437\u044b\u0432\u0430\u0435\u0442 \u043d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0443\u044e \u0441\u043b\u0443\u0436\u0431\u0443: `{service}`.\n\n\u042d\u0442\u043e \u043c\u043e\u0436\u0435\u0442 \u043f\u0440\u0435\u043f\u044f\u0442\u0441\u0442\u0432\u043e\u0432\u0430\u0442\u044c \u043a\u043e\u0440\u0440\u0435\u043a\u0442\u043d\u043e\u043c\u0443 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044e \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0438\u0438. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e, \u044d\u0442\u0430 \u0441\u043b\u0443\u0436\u0431\u0430 \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430, \u0430 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e, \u043f\u0440\u0438\u0447\u0438\u043d\u043e\u0439 \u0441\u0442\u0430\u043b\u0430 \u043e\u043f\u0435\u0447\u0430\u0442\u043a\u0430.\n\n\u0427\u0442\u043e\u0431\u044b \u0438\u0441\u043f\u0440\u0430\u0432\u0438\u0442\u044c \u044d\u0442\u0443 \u043e\u0448\u0438\u0431\u043a\u0443, [\u043e\u0442\u0440\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u0443\u0439\u0442\u0435 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0438\u044e]({edit}) \u0438\u043b\u0438 \u0443\u0434\u0430\u043b\u0438\u0442\u0435 \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0435, \u0432\u044b\u0437\u044b\u0432\u0430\u044e\u0449\u0435\u0435 \u044d\u0442\u0443 \u0441\u043b\u0443\u0436\u0431\u0443.\n\n\u041d\u0430\u0436\u043c\u0438\u0442\u0435 \u043a\u043d\u043e\u043f\u043a\u0443 \u041f\u041e\u0414\u0422\u0412\u0415\u0420\u0414\u0418\u0422\u042c, \u0447\u0442\u043e\u0431\u044b \u043e\u0442\u043c\u0435\u0442\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443 \u043a\u0430\u043a \u0443\u0441\u0442\u0440\u0430\u043d\u0451\u043d\u043d\u0443\u044e.", + "title": "{name} \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442 \u043d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0443\u044e \u0441\u043b\u0443\u0436\u0431\u0443" + } + } + }, + "title": "{name} \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442 \u043d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0443\u044e \u0441\u043b\u0443\u0436\u0431\u0443" + } + }, "state": { "_": { "off": "\u0412\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043e", diff --git a/homeassistant/components/automation/translations/sv.json b/homeassistant/components/automation/translations/sv.json index 8a5e2e58a9c..b506f524870 100644 --- a/homeassistant/components/automation/translations/sv.json +++ b/homeassistant/components/automation/translations/sv.json @@ -1,4 +1,17 @@ { + "issues": { + "service_not_found": { + "fix_flow": { + "step": { + "confirm": { + "description": "Automatiseringen \" {name} \" (` {entity_id} `) har en \u00e5tg\u00e4rd som anropar en ok\u00e4nd tj\u00e4nst: ` {service} `. \n\n Detta fel hindrar automatiseringen fr\u00e5n att fungera korrekt. Kanske \u00e4r den h\u00e4r tj\u00e4nsten inte l\u00e4ngre tillg\u00e4nglig, eller kanske ett stavfel orsakade det. \n\n F\u00f6r att \u00e5tg\u00e4rda detta fel, [redigera automatiseringen]( {edit} ) och ta bort \u00e5tg\u00e4rden som anropar den h\u00e4r tj\u00e4nsten. \n\n Klicka p\u00e5 Skicka nedan f\u00f6r att bekr\u00e4fta att du har \u00e5tg\u00e4rdat denna automatisering.", + "title": "{name} anv\u00e4nder en ok\u00e4nd tj\u00e4nst" + } + } + }, + "title": "{name} anv\u00e4nder en ok\u00e4nd tj\u00e4nst" + } + }, "state": { "_": { "off": "Av", diff --git a/homeassistant/components/automation/translations/tr.json b/homeassistant/components/automation/translations/tr.json index 804b616bfae..9d324a000a1 100644 --- a/homeassistant/components/automation/translations/tr.json +++ b/homeassistant/components/automation/translations/tr.json @@ -1,4 +1,17 @@ { + "issues": { + "service_not_found": { + "fix_flow": { + "step": { + "confirm": { + "description": "\" {name} \" (` {entity_id} `) otomasyonunun bilinmeyen bir hizmeti \u00e7a\u011f\u0131ran bir eylemi var: ` {service} `. \n\n Bu hata, otomasyonun do\u011fru \u015fekilde \u00e7al\u0131\u015fmas\u0131n\u0131 engeller. Belki bu hizmet art\u0131k mevcut de\u011fildir veya belki de bir yaz\u0131m hatas\u0131 buna neden olmu\u015ftur. \n\n Bu hatay\u0131 d\u00fczeltmek i\u00e7in [otomasyonu d\u00fczenleyin]( {edit} ) ve bu hizmeti \u00e7a\u011f\u0131ran eylemi kald\u0131r\u0131n. \n\n Bu otomasyonu d\u00fczeltti\u011finizi onaylamak i\u00e7in a\u015fa\u011f\u0131daki G\u00d6NDER'e t\u0131klay\u0131n.", + "title": "{name} bilinmeyen bir hizmet kullan\u0131yor" + } + } + }, + "title": "{name} bilinmeyen bir hizmet kullan\u0131yor" + } + }, "state": { "_": { "off": "Kapal\u0131", diff --git a/homeassistant/components/awair/config_flow.py b/homeassistant/components/awair/config_flow.py index becf6ce46ff..c68d46f7d39 100644 --- a/homeassistant/components/awair/config_flow.py +++ b/homeassistant/components/awair/config_flow.py @@ -10,7 +10,7 @@ from python_awair.exceptions import AuthError, AwairError from python_awair.user import AwairUser import voluptuous as vol -from homeassistant.components import zeroconf +from homeassistant.components import onboarding, zeroconf from homeassistant.config_entries import SOURCE_ZEROCONF, ConfigFlow from homeassistant.const import CONF_ACCESS_TOKEN, CONF_DEVICE, CONF_HOST from homeassistant.core import callback @@ -39,7 +39,10 @@ class AwairFlowHandler(ConfigFlow, domain=DOMAIN): if self._device is not None: await self.async_set_unique_id(self._device.mac_address) - self._abort_if_unique_id_configured(error="already_configured_device") + self._abort_if_unique_id_configured( + updates={CONF_HOST: self._device.device_addr}, + error="already_configured_device", + ) self.context.update( { "host": host, @@ -57,7 +60,7 @@ class AwairFlowHandler(ConfigFlow, domain=DOMAIN): self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Confirm discovery.""" - if user_input is not None: + if user_input is not None or not onboarding.async_is_onboarded(self.hass): title = f"{self._device.model} ({self._device.device_id})" return self.async_create_entry( title=title, diff --git a/homeassistant/components/awair/translations/bg.json b/homeassistant/components/awair/translations/bg.json index 1d5233cabbf..0f9884bf058 100644 --- a/homeassistant/components/awair/translations/bg.json +++ b/homeassistant/components/awair/translations/bg.json @@ -2,20 +2,55 @@ "config": { "abort": { "already_configured": "\u0410\u043a\u0430\u0443\u043d\u0442\u044a\u0442 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d", - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" + "already_configured_account": "\u0410\u043a\u0430\u0443\u043d\u0442\u044a\u0442 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d", + "already_configured_device": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u043e", + "no_devices_found": "\u041d\u044f\u043c\u0430 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0432 \u043c\u0440\u0435\u0436\u0430\u0442\u0430", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e", + "unreachable": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" }, "error": { - "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430", + "unreachable": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" }, + "flow_title": "{model} ({device_id})", "step": { + "cloud": { + "data": { + "email": "\u0418\u043c\u0435\u0439\u043b" + } + }, + "discovery_confirm": { + "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 {model} ({device_id})?" + }, + "local": { + "data": { + "host": "IP \u0430\u0434\u0440\u0435\u0441" + }, + "description": "\u0421\u043b\u0435\u0434\u0432\u0430\u0439\u0442\u0435 [\u0442\u0435\u0437\u0438 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u0438]({url}) \u0437\u0430 \u0442\u043e\u0432\u0430 \u043a\u0430\u043a \u0434\u0430 \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u0430\u0442\u0435 \u043b\u043e\u043a\u0430\u043b\u043d\u0438\u044f API \u043d\u0430 Awair.\n\n\u0429\u0440\u0430\u043a\u043d\u0435\u0442\u0435 \u0432\u044a\u0440\u0445\u0443 \"\u0418\u0417\u041f\u0420\u0410\u0429\u0410\u041d\u0415\", \u043a\u043e\u0433\u0430\u0442\u043e \u0441\u0442\u0435 \u0433\u043e\u0442\u043e\u0432\u0438." + }, + "local_pick": { + "data": { + "device": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e", + "host": "IP \u0430\u0434\u0440\u0435\u0441" + } + }, "reauth": { "data": { - "email": "Email" + "email": "\u0418\u043c\u0435\u0439\u043b" + } + }, + "reauth_confirm": { + "data": { + "email": "\u0418\u043c\u0435\u0439\u043b" } }, "user": { "data": { - "email": "Email" + "email": "\u0418\u043c\u0435\u0439\u043b" + }, + "menu_options": { + "cloud": "\u0421\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0447\u0440\u0435\u0437 \u043e\u0431\u043b\u0430\u043a\u0430", + "local": "\u0421\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u043b\u043e\u043a\u0430\u043b\u043d\u043e (\u0437\u0430 \u043f\u0440\u0435\u0434\u043f\u043e\u0447\u0438\u0442\u0430\u043d\u0435)" } } } diff --git a/homeassistant/components/awair/translations/cs.json b/homeassistant/components/awair/translations/cs.json index dfc83778bf9..6821388f2d4 100644 --- a/homeassistant/components/awair/translations/cs.json +++ b/homeassistant/components/awair/translations/cs.json @@ -2,14 +2,38 @@ "config": { "abort": { "already_configured": "\u00da\u010det je ji\u017e nastaven", + "already_configured_account": "\u00da\u010det je ji\u017e nastaven", + "already_configured_device": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", "no_devices_found": "V s\u00edti nebyla nalezena \u017e\u00e1dn\u00e1 za\u0159\u00edzen\u00ed", - "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", + "unreachable": "Nepoda\u0159ilo se p\u0159ipojit" }, "error": { "invalid_access_token": "Neplatn\u00fd p\u0159\u00edstupov\u00fd token", - "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba", + "unreachable": "Nepoda\u0159ilo se p\u0159ipojit" }, + "flow_title": "{model} ({device_id})", "step": { + "cloud": { + "data": { + "access_token": "P\u0159\u00edstupov\u00fd token", + "email": "E-mail" + } + }, + "discovery_confirm": { + "description": "Chcete nastavit {model} ({device_id})?" + }, + "local": { + "data": { + "host": "IP adresa" + } + }, + "local_pick": { + "data": { + "host": "IP adresa" + } + }, "reauth": { "data": { "access_token": "P\u0159\u00edstupov\u00fd token", diff --git a/homeassistant/components/awair/translations/es.json b/homeassistant/components/awair/translations/es.json index 82568ce9983..1f2508ec6e3 100644 --- a/homeassistant/components/awair/translations/es.json +++ b/homeassistant/components/awair/translations/es.json @@ -5,7 +5,7 @@ "already_configured_account": "La cuenta ya est\u00e1 configurada", "already_configured_device": "El dispositivo ya est\u00e1 configurado", "no_devices_found": "No se encontraron dispositivos en la red", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente", "unreachable": "No se pudo conectar" }, "error": { diff --git a/homeassistant/components/awair/translations/fr.json b/homeassistant/components/awair/translations/fr.json index 79101fd9128..5d9636eccee 100644 --- a/homeassistant/components/awair/translations/fr.json +++ b/homeassistant/components/awair/translations/fr.json @@ -29,7 +29,7 @@ "data": { "host": "Adresse IP" }, - "description": "Suivez [ces instructions]({url}) pour activer l\u2019API locale Awair.\n\nCliquez sur Envoyer apr\u00e8s avoir termin\u00e9." + "description": "Suivez [ces instructions]({url}) pour activer l\u2019API locale Awair.\n\nCliquez sur \u00ab\u00a0Valider\u00a0\u00bb apr\u00e8s avoir termin\u00e9." }, "local_pick": { "data": { diff --git a/homeassistant/components/awair/translations/hu.json b/homeassistant/components/awair/translations/hu.json index 1c940eed6c2..2e7be72e57f 100644 --- a/homeassistant/components/awair/translations/hu.json +++ b/homeassistant/components/awair/translations/hu.json @@ -56,7 +56,7 @@ "access_token": "Hozz\u00e1f\u00e9r\u00e9si token", "email": "E-mail" }, - "description": "Regisztr\u00e1lnia kell az Awair fejleszt\u0151i hozz\u00e1f\u00e9r\u00e9si tokenj\u00e9hez a k\u00f6vetkez\u0151 c\u00edmen: https://developer.getawair.com/onboard/login", + "description": "V\u00e1lassza a helyi lehet\u0151s\u00e9get a legjobb \u00e9lm\u00e9ny \u00e9rdek\u00e9ben. Csak akkor haszn\u00e1lja a felh\u0151t, ha az eszk\u00f6z nem ugyanahhoz a h\u00e1l\u00f3zathoz csatlakozik, mint a Home Assistant, vagy ha r\u00e9gebbi eszk\u00f6zzel rendelkezik.", "menu_options": { "cloud": "Felh\u0151n kereszt\u00fcli csatlakoz\u00e1s", "local": "Lok\u00e1lis csatlakoz\u00e1s (aj\u00e1nlott)" diff --git a/homeassistant/components/awair/translations/ja.json b/homeassistant/components/awair/translations/ja.json index a6bbe56c838..11c151082b9 100644 --- a/homeassistant/components/awair/translations/ja.json +++ b/homeassistant/components/awair/translations/ja.json @@ -29,7 +29,7 @@ "data": { "host": "IP\u30a2\u30c9\u30ec\u30b9" }, - "description": "\u6b21\u306e\u624b\u9806\u306b\u5f93\u3063\u3066\u3001Awair Local API\u3092\u6709\u52b9\u306b\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059: {url}" + "description": "[\u3053\u308c\u3089\u306e\u624b\u9806]( {url} ) \u306b\u5f93\u3063\u3066\u3001Awair Local API \u3092\u6709\u52b9\u306b\u3059\u308b\u65b9\u6cd5\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002 \n\n\u5b8c\u4e86\u3057\u305f\u3089\u9001\u4fe1\u3092\u30af\u30ea\u30c3\u30af\u3057\u307e\u3059\u3002" }, "local_pick": { "data": { diff --git a/homeassistant/components/awair/translations/nl.json b/homeassistant/components/awair/translations/nl.json index 1f002a5984b..7c70960c1b1 100644 --- a/homeassistant/components/awair/translations/nl.json +++ b/homeassistant/components/awair/translations/nl.json @@ -26,6 +26,12 @@ "host": "IP-adres" } }, + "local_pick": { + "data": { + "device": "Apparaat", + "host": "IP-adres" + } + }, "reauth": { "data": { "access_token": "Toegangstoken", @@ -44,7 +50,10 @@ "access_token": "Toegangstoken", "email": "E-mail" }, - "description": "U moet zich registreren voor een Awair-toegangstoken voor ontwikkelaars op: https://developer.getawair.com/onboard/login" + "description": "U moet zich registreren voor een Awair-toegangstoken voor ontwikkelaars op: https://developer.getawair.com/onboard/login", + "menu_options": { + "cloud": "Verbinden via de cloud" + } } } } diff --git a/homeassistant/components/awair/translations/pt.json b/homeassistant/components/awair/translations/pt.json index c906e6f380e..1f922776179 100644 --- a/homeassistant/components/awair/translations/pt.json +++ b/homeassistant/components/awair/translations/pt.json @@ -2,14 +2,41 @@ "config": { "abort": { "already_configured": "Conta j\u00e1 configurada", + "already_configured_account": "Conta j\u00e1 configurada", + "already_configured_device": "O dispositivo j\u00e1 est\u00e1 configurado", "no_devices_found": "Nenhum dispositivo encontrado na rede", - "reauth_successful": "Token de Acesso actualizado com sucesso" + "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida", + "unreachable": "Falha na liga\u00e7\u00e3o" }, "error": { "invalid_access_token": "Token de acesso inv\u00e1lido", - "unknown": "Erro inesperado" + "unknown": "Erro inesperado", + "unreachable": "Falha na liga\u00e7\u00e3o" }, + "flow_title": "{model} ( {device_id} )", "step": { + "cloud": { + "data": { + "access_token": "Token de Acesso", + "email": "Email" + }, + "description": "Voc\u00ea deve se registrar para um token de acesso de desenvolvedor Awair em: {url}" + }, + "discovery_confirm": { + "description": "Deseja configurar {model} ( {device_id} )?" + }, + "local": { + "data": { + "host": "Endere\u00e7o IP" + }, + "description": "Siga [estas instru\u00e7\u00f5es]( {url} ) sobre como ativar a API local Awair. \n\n Clique em enviar quando terminar." + }, + "local_pick": { + "data": { + "device": "Dispositivo", + "host": "Endere\u00e7o IP" + } + }, "reauth": { "data": { "access_token": "Token de Acesso", @@ -26,6 +53,10 @@ "data": { "access_token": "Token de Acesso", "email": "Email" + }, + "menu_options": { + "cloud": "Conecte-se pela nuvem", + "local": "Conecte-se localmente (preferencial)" } } } diff --git a/homeassistant/components/awair/translations/sv.json b/homeassistant/components/awair/translations/sv.json index 017247b4e1d..3fbdec53f83 100644 --- a/homeassistant/components/awair/translations/sv.json +++ b/homeassistant/components/awair/translations/sv.json @@ -2,14 +2,41 @@ "config": { "abort": { "already_configured": "Konto har redan konfigurerats", + "already_configured_account": "Konto har redan konfigurerats", + "already_configured_device": "Enheten \u00e4r redan konfigurerad", "no_devices_found": "Inga enheter hittades i n\u00e4tverket", - "reauth_successful": "\u00c5terautentisering lyckades" + "reauth_successful": "\u00c5terautentisering lyckades", + "unreachable": "Det gick inte att ansluta." }, "error": { "invalid_access_token": "Ogiltig \u00e5tkomstnyckel", - "unknown": "Ov\u00e4ntat fel" + "unknown": "Ov\u00e4ntat fel", + "unreachable": "Det gick inte att ansluta." }, + "flow_title": "{model} ({device_id})", "step": { + "cloud": { + "data": { + "access_token": "\u00c5tkomstnyckel", + "email": "E-post" + }, + "description": "Du m\u00e5ste registrera dig f\u00f6r en Awair-utvecklar\u00e5tkomsttoken p\u00e5: {url}" + }, + "discovery_confirm": { + "description": "Vill du konfigurera {model} ( {device_id} )?" + }, + "local": { + "data": { + "host": "IP-adress" + }, + "description": "F\u00f6lj [dessa instruktioner]({url}) om hur du aktiverar Awair Local API.\n\nKlicka p\u00e5 skicka n\u00e4r du \u00e4r klar." + }, + "local_pick": { + "data": { + "device": "Enhet", + "host": "IP-adress" + } + }, "reauth": { "data": { "access_token": "\u00c5tkomstnyckel", @@ -29,7 +56,11 @@ "access_token": "\u00c5tkomstnyckel", "email": "E-post" }, - "description": "Du m\u00e5ste registrera dig f\u00f6r en Awair-utvecklar\u00e5tkomsttoken p\u00e5: https://developer.getawair.com/onboard/login" + "description": "Du m\u00e5ste registrera dig f\u00f6r en Awair-utvecklar\u00e5tkomsttoken p\u00e5: https://developer.getawair.com/onboard/login", + "menu_options": { + "cloud": "Anslut via molnet", + "local": "Anslut lokalt (rekommenderas)" + } } } } diff --git a/homeassistant/components/awair/translations/tr.json b/homeassistant/components/awair/translations/tr.json index 8d49f985eae..87f90a564bc 100644 --- a/homeassistant/components/awair/translations/tr.json +++ b/homeassistant/components/awair/translations/tr.json @@ -56,7 +56,7 @@ "access_token": "Eri\u015fim Anahtar\u0131", "email": "E-posta" }, - "description": "Awair geli\u015ftirici eri\u015fim belirteci i\u00e7in \u015fu adresten kaydolmal\u0131s\u0131n\u0131z: https://developer.getawair.com/onboard/login", + "description": "En iyi deneyim i\u00e7in yerel se\u00e7in. Bulutu yaln\u0131zca cihaz Home Assistant ile ayn\u0131 a\u011fa ba\u011fl\u0131 de\u011filse veya eski bir cihaz\u0131n\u0131z varsa kullan\u0131n.", "menu_options": { "cloud": "Bulut \u00fczerinden ba\u011flan\u0131n", "local": "Yerel olarak ba\u011flan (tercih edilen)" diff --git a/homeassistant/components/aws/notify.py b/homeassistant/components/aws/notify.py index 37b49d44a88..c4e450a4aab 100644 --- a/homeassistant/components/aws/notify.py +++ b/homeassistant/components/aws/notify.py @@ -7,12 +7,12 @@ import logging from aiobotocore.session import AioSession from homeassistant.components.notify import ( + ATTR_DATA, ATTR_TARGET, ATTR_TITLE, ATTR_TITLE_DEFAULT, BaseNotificationService, ) -from homeassistant.components.notify.const import ATTR_DATA from homeassistant.const import ( CONF_NAME, CONF_PLATFORM, diff --git a/homeassistant/components/azure_devops/translations/es.json b/homeassistant/components/azure_devops/translations/es.json index 8414e03a727..a2a776f540c 100644 --- a/homeassistant/components/azure_devops/translations/es.json +++ b/homeassistant/components/azure_devops/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "error": { "cannot_connect": "No se pudo conectar", @@ -16,7 +16,7 @@ "personal_access_token": "Token Personal de Acceso (PAT)" }, "description": "Error de autenticaci\u00f3n para {project_url}. Por favor, introduce tus credenciales actuales.", - "title": "Reautenticaci\u00f3n" + "title": "Volver a autenticar" }, "user": { "data": { diff --git a/homeassistant/components/azure_devops/translations/pt.json b/homeassistant/components/azure_devops/translations/pt.json index b09f2cceda7..66fad0c2b08 100644 --- a/homeassistant/components/azure_devops/translations/pt.json +++ b/homeassistant/components/azure_devops/translations/pt.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Conta j\u00e1 configurada", - "reauth_successful": "Token de Acesso atualizado com sucesso" + "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida" }, "error": { "cannot_connect": "Falha na liga\u00e7\u00e3o", diff --git a/homeassistant/components/azure_event_hub/translations/ja.json b/homeassistant/components/azure_event_hub/translations/ja.json index 720e57d8066..717ce8f203b 100644 --- a/homeassistant/components/azure_event_hub/translations/ja.json +++ b/homeassistant/components/azure_event_hub/translations/ja.json @@ -2,9 +2,9 @@ "config": { "abort": { "already_configured": "\u30b5\u30fc\u30d3\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", - "cannot_connect": "configuration.yaml\u3092\u4f7f\u7528\u3057\u305f\u8a8d\u8a3c\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002yaml\u3092\u524a\u9664\u3057\u3066\u69cb\u6210\u30d5\u30ed\u30fc(config flow)\u3092\u4f7f\u7528\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "cannot_connect": "configuration.yaml \u304b\u3089\u306e\u8cc7\u683c\u60c5\u5831\u3067\u306e\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002yaml \u304b\u3089\u524a\u9664\u3057\u3066\u3001\u69cb\u6210\u30d5\u30ed\u30fc\u3092\u4f7f\u7528\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002", - "unknown": "configuration.yaml\u3092\u4f7f\u7528\u3057\u305f\u8a8d\u8a3c\u63a5\u7d9a\u304c\u4e0d\u660e\u306a\u30a8\u30e9\u30fc\u3067\u5931\u6557\u3057\u307e\u3057\u305f\u3002yaml\u3092\u524a\u9664\u3057\u3066\u69cb\u6210\u30d5\u30ed\u30fc(config flow)\u3092\u4f7f\u7528\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + "unknown": "configuration.yaml \u304b\u3089\u306e\u8cc7\u683c\u60c5\u5831\u3092\u4f7f\u7528\u3057\u305f\u63a5\u7d9a\u304c\u4e0d\u660e\u306a\u30a8\u30e9\u30fc\u3067\u5931\u6557\u3057\u307e\u3057\u305f\u3002yaml \u304b\u3089\u524a\u9664\u3057\u3066\u69cb\u6210\u30d5\u30ed\u30fc\u3092\u4f7f\u7528\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", diff --git a/homeassistant/components/azure_event_hub/translations/pt.json b/homeassistant/components/azure_event_hub/translations/pt.json index d252c078a2c..cf10963fbd4 100644 --- a/homeassistant/components/azure_event_hub/translations/pt.json +++ b/homeassistant/components/azure_event_hub/translations/pt.json @@ -1,7 +1,12 @@ { "config": { "abort": { - "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado", + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "unknown": "Erro inesperado" } } } \ No newline at end of file diff --git a/homeassistant/components/backup/manifest.json b/homeassistant/components/backup/manifest.json index eaf6a9fd979..3eefa68fcc4 100644 --- a/homeassistant/components/backup/manifest.json +++ b/homeassistant/components/backup/manifest.json @@ -6,5 +6,6 @@ "codeowners": ["@home-assistant/core"], "requirements": ["securetar==2022.2.0"], "iot_class": "calculated", - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "system" } diff --git a/homeassistant/components/baf/__init__.py b/homeassistant/components/baf/__init__.py index 7e80341deab..c9e51c79b82 100644 --- a/homeassistant/components/baf/__init__.py +++ b/homeassistant/components/baf/__init__.py @@ -5,6 +5,7 @@ import asyncio from aiobafi6 import Device, Service from aiobafi6.discovery import PORT +import async_timeout from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_IP_ADDRESS, Platform @@ -34,7 +35,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: run_future = device.async_run() try: - await asyncio.wait_for(device.async_wait_available(), timeout=RUN_TIMEOUT) + async with async_timeout.timeout(RUN_TIMEOUT): + await device.async_wait_available() except asyncio.TimeoutError as ex: run_future.cancel() raise ConfigEntryNotReady(f"Timed out connecting to {ip_address}") from ex diff --git a/homeassistant/components/baf/config_flow.py b/homeassistant/components/baf/config_flow.py index 2326d30937b..3f37df1b70a 100644 --- a/homeassistant/components/baf/config_flow.py +++ b/homeassistant/components/baf/config_flow.py @@ -7,6 +7,7 @@ from typing import Any from aiobafi6 import Device, Service from aiobafi6.discovery import PORT +import async_timeout import voluptuous as vol from homeassistant import config_entries @@ -26,7 +27,8 @@ async def async_try_connect(ip_address: str) -> Device: device = Device(Service(ip_addresses=[ip_address], port=PORT)) run_future = device.async_run() try: - await asyncio.wait_for(device.async_wait_available(), timeout=RUN_TIMEOUT) + async with async_timeout.timeout(RUN_TIMEOUT): + await device.async_wait_available() except asyncio.TimeoutError as ex: raise CannotConnect from ex finally: diff --git a/homeassistant/components/baf/translations/cs.json b/homeassistant/components/baf/translations/cs.json index 04f18366eaf..e33c65701c1 100644 --- a/homeassistant/components/baf/translations/cs.json +++ b/homeassistant/components/baf/translations/cs.json @@ -8,6 +8,9 @@ "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, "step": { + "discovery_confirm": { + "description": "Chcete nastavit {name} - {model} ({ip_address})?" + }, "user": { "data": { "ip_address": "IP adresa" diff --git a/homeassistant/components/baf/translations/pt.json b/homeassistant/components/baf/translations/pt.json index ce8a9287272..8dfe1f9f51e 100644 --- a/homeassistant/components/baf/translations/pt.json +++ b/homeassistant/components/baf/translations/pt.json @@ -2,6 +2,13 @@ "config": { "abort": { "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "step": { + "user": { + "data": { + "ip_address": "Endere\u00e7o IP" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/balboa/climate.py b/homeassistant/components/balboa/climate.py index cd10ccf2bc9..1cd93b4fddb 100644 --- a/homeassistant/components/balboa/climate.py +++ b/homeassistant/components/balboa/climate.py @@ -4,12 +4,12 @@ from __future__ import annotations import asyncio from typing import Any -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( FAN_HIGH, FAN_LOW, FAN_MEDIUM, FAN_OFF, + ClimateEntity, ClimateEntityFeature, HVACAction, HVACMode, diff --git a/homeassistant/components/balboa/const.py b/homeassistant/components/balboa/const.py index 9a2d6b704ff..dcd9c05ac91 100644 --- a/homeassistant/components/balboa/const.py +++ b/homeassistant/components/balboa/const.py @@ -3,7 +3,7 @@ from __future__ import annotations import logging -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( FAN_HIGH, FAN_LOW, FAN_MEDIUM, diff --git a/homeassistant/components/balboa/translations/cs.json b/homeassistant/components/balboa/translations/cs.json index e1bf8e7f45f..5eac883adf0 100644 --- a/homeassistant/components/balboa/translations/cs.json +++ b/homeassistant/components/balboa/translations/cs.json @@ -1,7 +1,18 @@ { "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + }, "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "host": "Hostitel" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/balboa/translations/pt.json b/homeassistant/components/balboa/translations/pt.json index f13cad90edc..04374af8e82 100644 --- a/homeassistant/components/balboa/translations/pt.json +++ b/homeassistant/components/balboa/translations/pt.json @@ -1,7 +1,11 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, "error": { - "cannot_connect": "Falha na liga\u00e7\u00e3o" + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "unknown": "Erro inesperado" }, "step": { "user": { diff --git a/homeassistant/components/bayesian/binary_sensor.py b/homeassistant/components/bayesian/binary_sensor.py index 5641480ba98..706c7ecdfd7 100644 --- a/homeassistant/components/bayesian/binary_sensor.py +++ b/homeassistant/components/bayesian/binary_sensor.py @@ -3,6 +3,7 @@ from __future__ import annotations from collections import OrderedDict import logging +from typing import Any import voluptuous as vol @@ -16,6 +17,7 @@ from homeassistant.const import ( CONF_PLATFORM, CONF_STATE, CONF_VALUE_TEMPLATE, + STATE_UNAVAILABLE, STATE_UNKNOWN, ) from homeassistant.core import HomeAssistant, callback @@ -33,6 +35,7 @@ from homeassistant.helpers.template import result_as_boolean from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import DOMAIN, PLATFORMS +from .repairs import raise_mirrored_entries, raise_no_prob_given_false ATTR_OBSERVATIONS = "observations" ATTR_OCCURRED_OBSERVATION_ENTITIES = "occurred_observation_entities" @@ -126,6 +129,16 @@ async def async_setup_platform( probability_threshold = config[CONF_PROBABILITY_THRESHOLD] device_class = config.get(CONF_DEVICE_CLASS) + # Should deprecate in some future version (2022.10 at time of writing) & make prob_given_false required in schemas. + broken_observations: list[dict[str, Any]] = [] + for observation in observations: + if CONF_P_GIVEN_F not in observation: + text: str = f"{name}/{observation.get(CONF_ENTITY_ID,'')}{observation.get(CONF_VALUE_TEMPLATE,'')}" + raise_no_prob_given_false(hass, observation, text) + _LOGGER.error("Missing prob_given_false YAML entry for %s", text) + broken_observations.append(observation) + observations = [x for x in observations if x not in broken_observations] + async_add_entities( [ BayesianBinarySensor( @@ -160,6 +173,7 @@ class BayesianBinarySensor(BinarySensorEntity): self.observation_handlers = { "numeric_state": self._process_numeric_state, "state": self._process_state, + "multi_state": self._process_multi_state, } async def async_added_to_hass(self) -> None: @@ -185,10 +199,6 @@ class BayesianBinarySensor(BinarySensorEntity): When a state changes, we must update our list of current observations, then calculate the new probability. """ - new_state = event.data.get("new_state") - - if new_state is None or new_state.state == STATE_UNKNOWN: - return entity = event.data.get("entity_id") @@ -210,7 +220,6 @@ class BayesianBinarySensor(BinarySensorEntity): template = track_template_result.template result = track_template_result.result entity = event and event.data.get("entity_id") - if isinstance(result, TemplateError): _LOGGER.error( "TemplateError('%s') " @@ -221,15 +230,12 @@ class BayesianBinarySensor(BinarySensorEntity): self.entity_id, ) - should_trigger = False + observation = None else: - should_trigger = result_as_boolean(result) + observation = result_as_boolean(result) for obs in self.observations_by_template[template]: - if should_trigger: - obs_entry = {"entity_id": entity, **obs} - else: - obs_entry = None + obs_entry = {"entity_id": entity, "observation": observation, **obs} self.current_observations[obs["id"]] = obs_entry if event: @@ -251,6 +257,22 @@ class BayesianBinarySensor(BinarySensorEntity): self.probability = self._calculate_new_probability() self._attr_is_on = bool(self.probability >= self._probability_threshold) + # detect mirrored entries + for entity, observations in self.observations_by_entity.items(): + raise_mirrored_entries( + self.hass, observations, text=f"{self._attr_name}/{entity}" + ) + + all_template_observations = [] + for value in self.observations_by_template.values(): + all_template_observations.append(value[0]) + if len(all_template_observations) == 2: + raise_mirrored_entries( + self.hass, + all_template_observations, + text=f"{self._attr_name}/{all_template_observations[0]['value_template']}", + ) + @callback def _recalculate_and_write_state(self): self.probability = self._calculate_new_probability() @@ -259,6 +281,7 @@ class BayesianBinarySensor(BinarySensorEntity): def _initialize_current_observations(self): local_observations = OrderedDict({}) + for entity in self.observations_by_entity: local_observations.update(self._record_entity_observations(entity)) return local_observations @@ -269,13 +292,13 @@ class BayesianBinarySensor(BinarySensorEntity): for entity_obs in self.observations_by_entity[entity]: platform = entity_obs["platform"] - should_trigger = self.observation_handlers[platform](entity_obs) - - if should_trigger: - obs_entry = {"entity_id": entity, **entity_obs} - else: - obs_entry = None + observation = self.observation_handlers[platform](entity_obs) + obs_entry = { + "entity_id": entity, + "observation": observation, + **entity_obs, + } local_observations[entity_obs["id"]] = obs_entry return local_observations @@ -285,11 +308,28 @@ class BayesianBinarySensor(BinarySensorEntity): for obs in self.current_observations.values(): if obs is not None: - prior = update_probability( - prior, - obs["prob_given_true"], - obs.get("prob_given_false", 1 - obs["prob_given_true"]), - ) + if obs["observation"] is True: + prior = update_probability( + prior, + obs["prob_given_true"], + obs["prob_given_false"], + ) + elif obs["observation"] is False: + prior = update_probability( + prior, + 1 - obs["prob_given_true"], + 1 - obs["prob_given_false"], + ) + elif obs["observation"] is None: + if obs["entity_id"] is not None: + _LOGGER.debug( + "Observation for entity '%s' returned None, it will not be used for Bayesian updating", + obs["entity_id"], + ) + else: + _LOGGER.debug( + "Observation for template entity returned None rather than a valid boolean, it will not be used for Bayesian updating", + ) return prior @@ -307,17 +347,21 @@ class BayesianBinarySensor(BinarySensorEntity): for all relevant observations to be looked up via their `entity_id`. """ - observations_by_entity = {} - for ind, obs in enumerate(self._observations): - obs["id"] = ind + observations_by_entity: dict[str, list[OrderedDict]] = {} + for i, obs in enumerate(self._observations): + obs["id"] = i if "entity_id" not in obs: continue + observations_by_entity.setdefault(obs["entity_id"], []).append(obs) - entity_ids = [obs["entity_id"]] - - for e_id in entity_ids: - observations_by_entity.setdefault(e_id, []).append(obs) + for li_of_dicts in observations_by_entity.values(): + if len(li_of_dicts) == 1: + continue + for ord_dict in li_of_dicts: + if ord_dict["platform"] != "state": + continue + ord_dict["platform"] = "multi_state" return observations_by_entity @@ -348,10 +392,12 @@ class BayesianBinarySensor(BinarySensorEntity): return observations_by_template def _process_numeric_state(self, entity_observation): - """Return True if numeric condition is met.""" + """Return True if numeric condition is met, return False if not, return None otherwise.""" entity = entity_observation["entity_id"] try: + if condition.state(self.hass, entity, [STATE_UNKNOWN, STATE_UNAVAILABLE]): + return None return condition.async_numeric_state( self.hass, entity, @@ -361,18 +407,31 @@ class BayesianBinarySensor(BinarySensorEntity): entity_observation, ) except ConditionError: - return False + return None def _process_state(self, entity_observation): """Return True if state conditions are met.""" entity = entity_observation["entity_id"] try: + if condition.state(self.hass, entity, [STATE_UNKNOWN, STATE_UNAVAILABLE]): + return None + return condition.state( self.hass, entity, entity_observation.get("to_state") ) except ConditionError: - return False + return None + + def _process_multi_state(self, entity_observation): + """Return True if state conditions are met.""" + entity = entity_observation["entity_id"] + + try: + if condition.state(self.hass, entity, entity_observation.get("to_state")): + return True + except ConditionError: + return None @property def extra_state_attributes(self): @@ -390,7 +449,9 @@ class BayesianBinarySensor(BinarySensorEntity): { obs.get("entity_id") for obs in self.current_observations.values() - if obs is not None and obs.get("entity_id") is not None + if obs is not None + and obs.get("entity_id") is not None + and obs.get("observation") is not None } ), ATTR_PROBABILITY: round(self.probability, 2), diff --git a/homeassistant/components/bayesian/manifest.json b/homeassistant/components/bayesian/manifest.json index 6a84beb1df6..1b5a466f0a2 100644 --- a/homeassistant/components/bayesian/manifest.json +++ b/homeassistant/components/bayesian/manifest.json @@ -2,7 +2,7 @@ "domain": "bayesian", "name": "Bayesian", "documentation": "https://www.home-assistant.io/integrations/bayesian", - "codeowners": [], + "codeowners": ["@HarvsG"], "quality_scale": "internal", "iot_class": "local_polling" } diff --git a/homeassistant/components/bayesian/repairs.py b/homeassistant/components/bayesian/repairs.py new file mode 100644 index 00000000000..a1d4f142527 --- /dev/null +++ b/homeassistant/components/bayesian/repairs.py @@ -0,0 +1,54 @@ +"""Helpers for generating repairs.""" +from __future__ import annotations + +from homeassistant.core import HomeAssistant +from homeassistant.helpers import issue_registry + +from . import DOMAIN + + +def raise_mirrored_entries(hass: HomeAssistant, observations, text: str = "") -> None: + """If there are mirrored entries, the user is probably using a workaround for a patched bug.""" + if len(observations) != 2: + return + true_sums_1: bool = ( + round( + observations[0]["prob_given_true"] + observations[1]["prob_given_true"], 1 + ) + == 1.0 + ) + false_sums_1: bool = ( + round( + observations[0]["prob_given_false"] + observations[1]["prob_given_false"], 1 + ) + == 1.0 + ) + same_states: bool = observations[0]["platform"] == observations[1]["platform"] + if true_sums_1 & false_sums_1 & same_states: + issue_registry.async_create_issue( + hass, + DOMAIN, + "mirrored_entry/" + text, + breaks_in_ha_version="2022.10.0", + is_fixable=False, + severity=issue_registry.IssueSeverity.WARNING, + translation_key="manual_migration", + translation_placeholders={"entity": text}, + learn_more_url="https://github.com/home-assistant/core/pull/67631", + ) + + +# Should deprecate in some future version (2022.10 at time of writing) & make prob_given_false required in schemas. +def raise_no_prob_given_false(hass: HomeAssistant, observation, text: str) -> None: + """In previous 2022.9 and earlier, prob_given_false was optional and had a default version.""" + issue_registry.async_create_issue( + hass, + DOMAIN, + f"no_prob_given_false/{text}", + breaks_in_ha_version="2022.10.0", + is_fixable=False, + severity=issue_registry.IssueSeverity.ERROR, + translation_key="no_prob_given_false", + translation_placeholders={"entity": text}, + learn_more_url="https://github.com/home-assistant/core/pull/67631", + ) diff --git a/homeassistant/components/bayesian/strings.json b/homeassistant/components/bayesian/strings.json new file mode 100644 index 00000000000..338795624cd --- /dev/null +++ b/homeassistant/components/bayesian/strings.json @@ -0,0 +1,12 @@ +{ + "issues": { + "manual_migration": { + "description": "The Bayesian integration now also updates the probability if the observed `to_state`, `above`, `below`, or `value_template` evaluates to `False` rather than only `True`. So it is no longer required to have duplicate, complementary entries for each binary state. Please remove the mirrored entry for `{entity}`.", + "title": "Manual YAML fix required for Bayesian" + }, + "no_prob_given_false": { + "description": "In the Bayesian integration `prob_given_false` is now a required configuration variable as there was no mathematical rationale for the previous default value. Please add this to your `configuration.yml` for `bayesian/{entity}`. These observations will be ignored until you do.", + "title": "Manual YAML addition required for Bayesian" + } + } +} diff --git a/homeassistant/components/bayesian/translations/en.json b/homeassistant/components/bayesian/translations/en.json new file mode 100644 index 00000000000..f95e153d986 --- /dev/null +++ b/homeassistant/components/bayesian/translations/en.json @@ -0,0 +1,12 @@ +{ + "issues": { + "manual_migration": { + "description": "The Bayesian integration now also updates the probability if the observed `to_state`, `above`, `below`, or `value_template` evaluates to `False` rather than only `True`. So it is no longer required to have duplicate, complementary entries for each binary state. Please remove the mirrored entry for `{entity}`.", + "title": "Manual YAML fix required for Bayesian" + }, + "no_prob_given_false": { + "description": "In the Bayesian integration `prob_given_false` is now a required configuration variable as there was no mathematical rationale for the previous default value. Please add this to your `configuration.yml` for `bayesian/{entity}`. These observations will be ignored until you do.", + "title": "Manual YAML addition required for Bayesian" + } + } +} diff --git a/homeassistant/components/bbox/device_tracker.py b/homeassistant/components/bbox/device_tracker.py index ab8e7a72441..a9b0312673b 100644 --- a/homeassistant/components/bbox/device_tracker.py +++ b/homeassistant/components/bbox/device_tracker.py @@ -31,7 +31,7 @@ PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( ) -def get_scanner(hass: HomeAssistant, config: ConfigType) -> DeviceScanner | None: +def get_scanner(hass: HomeAssistant, config: ConfigType) -> BboxDeviceScanner | None: """Validate the configuration and return a Bbox scanner.""" scanner = BboxDeviceScanner(config[DOMAIN]) diff --git a/homeassistant/components/binary_sensor/__init__.py b/homeassistant/components/binary_sensor/__init__.py index 82e903ce877..46107938ddf 100644 --- a/homeassistant/components/binary_sensor/__init__.py +++ b/homeassistant/components/binary_sensor/__init__.py @@ -150,10 +150,12 @@ DEVICE_CLASS_UPDATE = BinarySensorDeviceClass.UPDATE.value DEVICE_CLASS_VIBRATION = BinarySensorDeviceClass.VIBRATION.value DEVICE_CLASS_WINDOW = BinarySensorDeviceClass.WINDOW.value +# mypy: disallow-any-generics + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Track states and offer events for binary sensors.""" - component = hass.data[DOMAIN] = EntityComponent( + component = hass.data[DOMAIN] = EntityComponent[BinarySensorEntity]( logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL ) @@ -163,13 +165,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry.""" - component: EntityComponent = hass.data[DOMAIN] + component: EntityComponent[BinarySensorEntity] = hass.data[DOMAIN] return await component.async_setup_entry(entry) async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - component: EntityComponent = hass.data[DOMAIN] + component: EntityComponent[BinarySensorEntity] = hass.data[DOMAIN] return await component.async_unload_entry(entry) diff --git a/homeassistant/components/binary_sensor/device_condition.py b/homeassistant/components/binary_sensor/device_condition.py index 4b3aa70d716..4da9bd45670 100644 --- a/homeassistant/components/binary_sensor/device_condition.py +++ b/homeassistant/components/binary_sensor/device_condition.py @@ -3,7 +3,7 @@ from __future__ import annotations import voluptuous as vol -from homeassistant.components.device_automation.const import CONF_IS_OFF, CONF_IS_ON +from homeassistant.components.device_automation import CONF_IS_OFF, CONF_IS_ON from homeassistant.const import ( CONF_CONDITION, CONF_ENTITY_ID, diff --git a/homeassistant/components/binary_sensor/device_trigger.py b/homeassistant/components/binary_sensor/device_trigger.py index a6dfc762d45..969a52d1514 100644 --- a/homeassistant/components/binary_sensor/device_trigger.py +++ b/homeassistant/components/binary_sensor/device_trigger.py @@ -1,10 +1,10 @@ """Provides device triggers for binary sensors.""" import voluptuous as vol -from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA -from homeassistant.components.device_automation.const import ( +from homeassistant.components.device_automation import ( CONF_TURNED_OFF, CONF_TURNED_ON, + DEVICE_TRIGGER_BASE_SCHEMA, ) from homeassistant.components.homeassistant.triggers import state as state_trigger from homeassistant.const import CONF_ENTITY_ID, CONF_FOR, CONF_TYPE @@ -16,8 +16,6 @@ from homeassistant.helpers.typing import ConfigType from . import DOMAIN, BinarySensorDeviceClass -# mypy: allow-untyped-defs, no-check-untyped-defs - DEVICE_CLASS_NONE = "none" CONF_BAT_LOW = "bat_low" diff --git a/homeassistant/components/binary_sensor/manifest.json b/homeassistant/components/binary_sensor/manifest.json index d1c631ee94b..e10478889f3 100644 --- a/homeassistant/components/binary_sensor/manifest.json +++ b/homeassistant/components/binary_sensor/manifest.json @@ -3,5 +3,6 @@ "name": "Binary Sensor", "documentation": "https://www.home-assistant.io/integrations/binary_sensor", "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "entity" } diff --git a/homeassistant/components/binary_sensor/translations/hu.json b/homeassistant/components/binary_sensor/translations/hu.json index 56df2994766..ad5d5d93254 100644 --- a/homeassistant/components/binary_sensor/translations/hu.json +++ b/homeassistant/components/binary_sensor/translations/hu.json @@ -110,7 +110,7 @@ "cold": "h\u0171t\u00e9s", "gas": "g\u00e1z", "heat": "f\u0171t\u00e9s", - "moisture": "nedvess\u00e9g", + "moisture": "nedvess\u00e9gtartalom", "motion": "mozg\u00e1s", "occupancy": "foglalts\u00e1g", "power": "teljes\u00edtm\u00e9ny", diff --git a/homeassistant/components/binary_sensor/translations/pt-BR.json b/homeassistant/components/binary_sensor/translations/pt-BR.json index 5bd166f9367..5fd4939a015 100644 --- a/homeassistant/components/binary_sensor/translations/pt-BR.json +++ b/homeassistant/components/binary_sensor/translations/pt-BR.json @@ -94,7 +94,7 @@ "powered": "{entity_name} alimentado", "present": "{entity_name} presente", "problem": "{entity_name} come\u00e7ou a detectar problema", - "running": "{entity_name} come\u00e7ar a correr", + "running": "{nome_da_entidade} come\u00e7ou a executar", "smoke": "{entity_name} come\u00e7ou a detectar fuma\u00e7a", "sound": "{entity_name} come\u00e7ou a detectar som", "tampered": "{entity_name} come\u00e7ou a detectar adultera\u00e7\u00e3o", diff --git a/homeassistant/components/binary_sensor/translations/pt.json b/homeassistant/components/binary_sensor/translations/pt.json index cfaf2e36bd3..6245d291f42 100644 --- a/homeassistant/components/binary_sensor/translations/pt.json +++ b/homeassistant/components/binary_sensor/translations/pt.json @@ -109,6 +109,7 @@ "on": "A carregar" }, "carbon_monoxide": { + "off": "Limpo", "on": "Detectado" }, "cold": { diff --git a/homeassistant/components/blackbird/media_player.py b/homeassistant/components/blackbird/media_player.py index b0fda2de0f4..5e6e996c7dd 100644 --- a/homeassistant/components/blackbird/media_player.py +++ b/homeassistant/components/blackbird/media_player.py @@ -12,6 +12,7 @@ from homeassistant.components.media_player import ( PLATFORM_SCHEMA, MediaPlayerEntity, MediaPlayerEntityFeature, + MediaPlayerState, ) from homeassistant.const import ( ATTR_ENTITY_ID, @@ -19,8 +20,6 @@ from homeassistant.const import ( CONF_NAME, CONF_PORT, CONF_TYPE, - STATE_OFF, - STATE_ON, ) from homeassistant.core import HomeAssistant, ServiceCall import homeassistant.helpers.config_validation as cv @@ -163,7 +162,7 @@ class BlackbirdZone(MediaPlayerEntity): state = self._blackbird.zone_status(self._zone_id) if not state: return - self._attr_state = STATE_ON if state.power else STATE_OFF + self._attr_state = MediaPlayerState.ON if state.power else MediaPlayerState.OFF idx = state.av if idx in self._source_id_name: self._attr_source = self._source_id_name[idx] diff --git a/homeassistant/components/blebox/climate.py b/homeassistant/components/blebox/climate.py index 78f47b1ba46..65920b170c5 100644 --- a/homeassistant/components/blebox/climate.py +++ b/homeassistant/components/blebox/climate.py @@ -2,8 +2,8 @@ from datetime import timedelta from typing import Any -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( + ClimateEntity, ClimateEntityFeature, HVACAction, HVACMode, diff --git a/homeassistant/components/bluemaestro/translations/bg.json b/homeassistant/components/bluemaestro/translations/bg.json new file mode 100644 index 00000000000..2ddd9134286 --- /dev/null +++ b/homeassistant/components/bluemaestro/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 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u043e", + "no_devices_found": "\u041d\u044f\u043c\u0430 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0432 \u043c\u0440\u0435\u0436\u0430\u0442\u0430", + "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/bluemaestro/translations/bn.json b/homeassistant/components/bluemaestro/translations/bn.json new file mode 100644 index 00000000000..cfef9be6dac --- /dev/null +++ b/homeassistant/components/bluemaestro/translations/bn.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u09a1\u09bf\u09ad\u09be\u0987\u09b8 \u0987\u09a4\u09bf\u09ae\u09a7\u09cd\u09af\u09c7 \u0995\u09a8\u09ab\u09bf\u0997\u09be\u09b0 \u0995\u09b0\u09be \u0986\u099b\u09c7", + "already_in_progress": "\u0995\u09a8\u09ab\u09bf\u0997\u09be\u09b0\u09c7\u09b6\u09a8 \u09aa\u09cd\u09b0\u09ac\u09be\u09b9 \u0987\u09a4\u09bf\u09ae\u09a7\u09cd\u09af\u09c7\u0987 \u099a\u09b2\u099b\u09c7", + "no_devices_found": "\u09a8\u09c7\u099f\u0993\u09af\u09bc\u09be\u09b0\u09cd\u0995\u09c7 \u0995\u09cb\u09a8\u09cb \u09a1\u09bf\u09ad\u09be\u0987\u09b8 \u09aa\u09be\u0993\u09af\u09bc\u09be \u09af\u09be\u09af\u09bc\u09a8\u09bf", + "not_supported": "\u09a1\u09bf\u09ad\u09be\u0987\u09b8 \u09b8\u09ae\u09b0\u09cd\u09a5\u09bf\u09a4 \u09a8\u09af\u09bc" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u0986\u09aa\u09a8\u09bf \u0995\u09bf {name} \u09b8\u09c7\u099f\u0986\u09aa \u0995\u09b0\u09a4\u09c7 \u099a\u09be\u09a8?" + }, + "user": { + "data": { + "address": "\u09a1\u09bf\u09ad\u09be\u0987\u09b8" + }, + "description": "\u09b8\u09c7\u099f\u0986\u09aa \u0995\u09b0\u09be\u09b0 \u099c\u09a8\u09cd\u09af \u098f\u0995\u099f\u09bf \u09a1\u09bf\u09ad\u09be\u0987\u09b8 \u099a\u09af\u09bc\u09a8 \u0995\u09b0\u09c1\u09a8" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bluemaestro/translations/ca.json b/homeassistant/components/bluemaestro/translations/ca.json new file mode 100644 index 00000000000..c121ff7408c --- /dev/null +++ b/homeassistant/components/bluemaestro/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/bluemaestro/translations/cs.json b/homeassistant/components/bluemaestro/translations/cs.json new file mode 100644 index 00000000000..1163b27775a --- /dev/null +++ b/homeassistant/components/bluemaestro/translations/cs.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "already_in_progress": "Konfigurace ji\u017e prob\u00edh\u00e1", + "no_devices_found": "V s\u00edti nebyla nalezena \u017e\u00e1dn\u00e1 za\u0159\u00edzen\u00ed", + "not_supported": "Za\u0159\u00edzen\u00ed nen\u00ed podporov\u00e1no" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Chcete nastavit {name}?" + }, + "user": { + "data": { + "address": "Za\u0159\u00edzen\u00ed" + }, + "description": "Zvolte za\u0159\u00edzen\u00ed, kter\u00e9 chcete nastavit" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bluemaestro/translations/de.json b/homeassistant/components/bluemaestro/translations/de.json new file mode 100644 index 00000000000..4c5720ec6fb --- /dev/null +++ b/homeassistant/components/bluemaestro/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/bluemaestro/translations/el.json b/homeassistant/components/bluemaestro/translations/el.json new file mode 100644 index 00000000000..cdb57c8ac1b --- /dev/null +++ b/homeassistant/components/bluemaestro/translations/el.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", + "already_in_progress": "\u0397 \u03c1\u03bf\u03ae \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2 \u03b2\u03c1\u03af\u03c3\u03ba\u03b5\u03c4\u03b1\u03b9 \u03ae\u03b4\u03b7 \u03c3\u03b5 \u03b5\u03be\u03ad\u03bb\u03b9\u03be\u03b7", + "no_devices_found": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03c3\u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03bf", + "not_supported": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {name};" + }, + "user": { + "data": { + "address": "\u03a3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae" + }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b3\u03b9\u03b1 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bluemaestro/translations/es.json b/homeassistant/components/bluemaestro/translations/es.json new file mode 100644 index 00000000000..ae0ab01acdf --- /dev/null +++ b/homeassistant/components/bluemaestro/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/bluemaestro/translations/et.json b/homeassistant/components/bluemaestro/translations/et.json new file mode 100644 index 00000000000..2cfcdd2b591 --- /dev/null +++ b/homeassistant/components/bluemaestro/translations/et.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "already_in_progress": "H\u00e4\u00e4lestamine juba k\u00e4ib", + "no_devices_found": "V\u00f5rgust seadmeid ei leitud", + "not_supported": "Seadet ei toetata" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Kas h\u00e4\u00e4lestada seade {name}?" + }, + "user": { + "data": { + "address": "Seade" + }, + "description": "Vali h\u00e4\u00e4lestatav seade" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bluemaestro/translations/fr.json b/homeassistant/components/bluemaestro/translations/fr.json new file mode 100644 index 00000000000..8ddb4af4dbc --- /dev/null +++ b/homeassistant/components/bluemaestro/translations/fr.json @@ -0,0 +1,22 @@ +{ + "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", + "not_supported": "Appareil non pris en charge" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Voulez-vous configurer {name}\u00a0?" + }, + "user": { + "data": { + "address": "Appareil" + }, + "description": "S\u00e9lectionnez l'appareil \u00e0 configurer" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bluemaestro/translations/he.json b/homeassistant/components/bluemaestro/translations/he.json new file mode 100644 index 00000000000..de780eb221a --- /dev/null +++ b/homeassistant/components/bluemaestro/translations/he.json @@ -0,0 +1,21 @@ +{ + "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 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea {name}?" + }, + "user": { + "data": { + "address": "\u05d4\u05ea\u05e7\u05df" + }, + "description": "\u05d1\u05d7\u05d9\u05e8\u05ea \u05d4\u05ea\u05e7\u05df \u05dc\u05d4\u05d2\u05d3\u05e8\u05d4" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bluemaestro/translations/hu.json b/homeassistant/components/bluemaestro/translations/hu.json new file mode 100644 index 00000000000..97fbb5b9408 --- /dev/null +++ b/homeassistant/components/bluemaestro/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\u00e1lassza ki a be\u00e1ll\u00edtani k\u00edv\u00e1nt eszk\u00f6zt" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bluemaestro/translations/id.json b/homeassistant/components/bluemaestro/translations/id.json new file mode 100644 index 00000000000..573eb39ed15 --- /dev/null +++ b/homeassistant/components/bluemaestro/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/bluemaestro/translations/it.json b/homeassistant/components/bluemaestro/translations/it.json new file mode 100644 index 00000000000..7784ed3a240 --- /dev/null +++ b/homeassistant/components/bluemaestro/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": "Seleziona un dispositivo da configurare" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bluemaestro/translations/ja.json b/homeassistant/components/bluemaestro/translations/ja.json new file mode 100644 index 00000000000..7e4f5db8e3b --- /dev/null +++ b/homeassistant/components/bluemaestro/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/bluemaestro/translations/nl.json b/homeassistant/components/bluemaestro/translations/nl.json new file mode 100644 index 00000000000..281d6feff46 --- /dev/null +++ b/homeassistant/components/bluemaestro/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 is 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/bluemaestro/translations/no.json b/homeassistant/components/bluemaestro/translations/no.json new file mode 100644 index 00000000000..0bf8b1695ec --- /dev/null +++ b/homeassistant/components/bluemaestro/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 konfigurere {name}?" + }, + "user": { + "data": { + "address": "Enhet" + }, + "description": "Velg en enhet du vil konfigurere" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bluemaestro/translations/pl.json b/homeassistant/components/bluemaestro/translations/pl.json new file mode 100644 index 00000000000..4715905a2e9 --- /dev/null +++ b/homeassistant/components/bluemaestro/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/bluemaestro/translations/pt-BR.json b/homeassistant/components/bluemaestro/translations/pt-BR.json new file mode 100644 index 00000000000..5b654163201 --- /dev/null +++ b/homeassistant/components/bluemaestro/translations/pt-BR.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "O fluxo de configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", + "no_devices_found": "Nenhum dispositivo encontrado na rede", + "not_supported": "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/bluemaestro/translations/pt.json b/homeassistant/components/bluemaestro/translations/pt.json new file mode 100644 index 00000000000..5a10362e52b --- /dev/null +++ b/homeassistant/components/bluemaestro/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "not_supported": "Dispositivo n\u00e3o suportado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bluemaestro/translations/ru.json b/homeassistant/components/bluemaestro/translations/ru.json new file mode 100644 index 00000000000..887499e5f2e --- /dev/null +++ b/homeassistant/components/bluemaestro/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/bluemaestro/translations/sv.json b/homeassistant/components/bluemaestro/translations/sv.json new file mode 100644 index 00000000000..6c6f3f5f1bb --- /dev/null +++ b/homeassistant/components/bluemaestro/translations/sv.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "already_in_progress": "Konfigurationsfl\u00f6det p\u00e5g\u00e5r redan", + "no_devices_found": "Inga enheter hittades i n\u00e4tverket", + "not_supported": "Enheten st\u00f6ds inte" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Vill du konfigurera {name}?" + }, + "user": { + "data": { + "address": "Enhet" + }, + "description": "V\u00e4lj en enhet att konfigurera" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bluemaestro/translations/tr.json b/homeassistant/components/bluemaestro/translations/tr.json new file mode 100644 index 00000000000..f0ddbc274c9 --- /dev/null +++ b/homeassistant/components/bluemaestro/translations/tr.json @@ -0,0 +1,22 @@ +{ + "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", + "not_supported": "Cihaz desteklenmiyor" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "{name} kurulumunu yapmak istiyor musunuz?" + }, + "user": { + "data": { + "address": "Cihaz" + }, + "description": "Kurulum i\u00e7in bir cihaz se\u00e7in" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bluemaestro/translations/zh-Hant.json b/homeassistant/components/bluemaestro/translations/zh-Hant.json new file mode 100644 index 00000000000..64ae1f19094 --- /dev/null +++ b/homeassistant/components/bluemaestro/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/blueprint/manifest.json b/homeassistant/components/blueprint/manifest.json index c00b92b1e3c..4ed299438bb 100644 --- a/homeassistant/components/blueprint/manifest.json +++ b/homeassistant/components/blueprint/manifest.json @@ -3,5 +3,6 @@ "name": "Blueprint", "documentation": "https://www.home-assistant.io/integrations/blueprint", "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "system" } diff --git a/homeassistant/components/bluesound/media_player.py b/homeassistant/components/bluesound/media_player.py index 53f6de14b3c..23611e5bef5 100644 --- a/homeassistant/components/bluesound/media_player.py +++ b/homeassistant/components/bluesound/media_player.py @@ -19,14 +19,13 @@ import xmltodict from homeassistant.components import media_source from homeassistant.components.media_player import ( PLATFORM_SCHEMA, + BrowseMedia, MediaPlayerEntity, MediaPlayerEntityFeature, -) -from homeassistant.components.media_player.browse_media import ( - BrowseMedia, + MediaPlayerState, + MediaType, async_process_play_media_url, ) -from homeassistant.components.media_player.const import MEDIA_TYPE_MUSIC from homeassistant.const import ( ATTR_ENTITY_ID, CONF_HOST, @@ -35,10 +34,6 @@ from homeassistant.const import ( CONF_PORT, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, - STATE_IDLE, - STATE_OFF, - STATE_PAUSED, - STATE_PLAYING, ) from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -68,7 +63,6 @@ DEFAULT_PORT = 11000 NODE_OFFLINE_CHECK_TIMEOUT = 180 NODE_RETRY_INITIATION = timedelta(minutes=3) -STATE_GROUPED = "grouped" SYNC_STATUS_INTERVAL = timedelta(minutes=5) UPDATE_CAPTURE_INTERVAL = timedelta(minutes=30) @@ -205,6 +199,8 @@ async def async_setup_platform( class BluesoundPlayer(MediaPlayerEntity): """Representation of a Bluesound Player.""" + _attr_media_content_type = MediaType.MUSIC + def __init__(self, hass, host, port=None, name=None, init_callback=None): """Initialize the media player.""" self.host = host @@ -552,25 +548,20 @@ class BluesoundPlayer(MediaPlayerEntity): return self._services_items @property - def media_content_type(self): - """Content type of current playing media.""" - return MEDIA_TYPE_MUSIC - - @property - def state(self): + def state(self) -> MediaPlayerState: """Return the state of the device.""" if self._status is None: - return STATE_OFF + return MediaPlayerState.OFF if self.is_grouped and not self.is_master: - return STATE_GROUPED + return MediaPlayerState.IDLE status = self._status.get("state") if status in ("pause", "stop"): - return STATE_PAUSED + return MediaPlayerState.PAUSED if status in ("stream", "play"): - return STATE_PLAYING - return STATE_IDLE + return MediaPlayerState.PLAYING + return MediaPlayerState.IDLE @property def media_title(self): @@ -623,14 +614,14 @@ class BluesoundPlayer(MediaPlayerEntity): return None mediastate = self.state - if self._last_status_update is None or mediastate == STATE_IDLE: + if self._last_status_update is None or mediastate == MediaPlayerState.IDLE: return None if (position := self._status.get("secs")) is None: return None position = float(position) - if mediastate == STATE_PLAYING: + if mediastate == MediaPlayerState.PLAYING: position += (dt_util.utcnow() - self._last_status_update).total_seconds() return position @@ -1022,7 +1013,7 @@ class BluesoundPlayer(MediaPlayerEntity): return await self.send_bluesound_command(f"Play?seek={float(position)}") 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_media command to the media player.""" if self.is_grouped and not self.is_master: @@ -1069,7 +1060,9 @@ class BluesoundPlayer(MediaPlayerEntity): return await self.send_bluesound_command("Volume?mute=0") async def async_browse_media( - self, media_content_type: str | None = None, media_content_id: str | None = None + self, + media_content_type: MediaType | str | None = None, + media_content_id: str | None = None, ) -> BrowseMedia: """Implement the websocket media browsing helper.""" return await media_source.async_browse_media( diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index 8ba3a503a30..f175b01b798 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -8,14 +8,24 @@ import platform from typing import TYPE_CHECKING, cast import async_timeout +from awesomeversion import AwesomeVersion from homeassistant.components import usb -from homeassistant.config_entries import SOURCE_INTEGRATION_DISCOVERY, ConfigEntry -from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.config_entries import ( + SOURCE_IGNORE, + SOURCE_INTEGRATION_DISCOVERY, + ConfigEntry, +) +from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback as hass_callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr, discovery_flow from homeassistant.helpers.debounce import Debouncer +from homeassistant.helpers.issue_registry import ( + IssueSeverity, + async_create_issue, + async_delete_issue, +) from homeassistant.loader import async_get_bluetooth from . import models @@ -42,9 +52,10 @@ from .models import ( BluetoothServiceInfo, BluetoothServiceInfoBleak, HaBleakScannerWrapper, + HaBluetoothConnector, ProcessAdvertisementCallback, ) -from .scanner import HaScanner, ScannerStartError, create_bleak_scanner +from .scanner import HaScanner, ScannerStartError from .util import adapter_human_name, adapter_unique_name, async_default_adapter if TYPE_CHECKING: @@ -56,9 +67,11 @@ __all__ = [ "async_ble_device_from_address", "async_discovered_service_info", "async_get_scanner", + "async_last_service_info", "async_process_advertisements", "async_rediscover_address", "async_register_callback", + "async_register_scanner", "async_track_unavailable", "async_scanner_count", "BaseHaScanner", @@ -66,11 +79,14 @@ __all__ = [ "BluetoothServiceInfoBleak", "BluetoothScanningMode", "BluetoothCallback", + "HaBluetoothConnector", "SOURCE_LOCAL", ] _LOGGER = logging.getLogger(__name__) +RECOMMENDED_MIN_HAOS_VERSION = AwesomeVersion("9.0.dev0") + def _get_manager(hass: HomeAssistant) -> BluetoothManager: """Get the bluetooth manager.""" @@ -103,6 +119,16 @@ def async_discovered_service_info( return _get_manager(hass).async_discovered_service_info(connectable) +@hass_callback +def async_last_service_info( + hass: HomeAssistant, address: str, connectable: bool = True +) -> BluetoothServiceInfoBleak | None: + """Return the last service info for an address.""" + if DATA_MANAGER not in hass.data: + return None + return _get_manager(hass).async_last_service_info(address, connectable) + + @hass_callback def async_ble_device_from_address( hass: HomeAssistant, address: str, connectable: bool = True @@ -173,7 +199,7 @@ async def async_process_advertisements( @hass_callback def async_track_unavailable( hass: HomeAssistant, - callback: Callable[[str], None], + callback: Callable[[BluetoothServiceInfoBleak], None], address: str, connectable: bool = True, ) -> Callable[[], None]: @@ -213,12 +239,49 @@ async def async_get_adapter_from_address( return await _get_manager(hass).async_get_adapter_from_address(address) +@hass_callback +def _async_haos_is_new_enough(hass: HomeAssistant) -> bool: + """Check if the version of Home Assistant Operating System is new enough.""" + # Only warn if a USB adapter is plugged in + if not any( + entry + for entry in hass.config_entries.async_entries(DOMAIN) + if entry.source != SOURCE_IGNORE + ): + return True + if ( + not hass.components.hassio.is_hassio() + or not (os_info := hass.components.hassio.get_os_info()) + or not (haos_version := os_info.get("version")) + or AwesomeVersion(haos_version) >= RECOMMENDED_MIN_HAOS_VERSION + ): + return True + return False + + +@hass_callback +def _async_check_haos(hass: HomeAssistant) -> None: + """Create or delete an the haos_outdated issue.""" + if _async_haos_is_new_enough(hass): + async_delete_issue(hass, DOMAIN, "haos_outdated") + return + async_create_issue( + hass, + DOMAIN, + "haos_outdated", + is_fixable=False, + severity=IssueSeverity.WARNING, + learn_more_url="/config/updates", + translation_key="haos_outdated", + ) + + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the bluetooth integration.""" integration_matcher = IntegrationMatcher(await async_get_bluetooth(hass)) integration_matcher.async_setup() manager = BluetoothManager(hass, integration_matcher) - manager.async_setup() + await manager.async_setup() hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, manager.async_stop) hass.data[DATA_MANAGER] = models.MANAGER = manager adapters = await manager.async_get_bluetooth_adapters() @@ -251,6 +314,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: EVENT_HOMEASSISTANT_STOP, hass_callback(lambda event: cancel()) ) + # Wait to check until after start to make sure + # that the system info is available. + hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STARTED, + hass_callback(lambda event: _async_check_haos(hass)), + ) + return True @@ -330,13 +400,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: passive = entry.options.get(CONF_PASSIVE) mode = BluetoothScanningMode.PASSIVE if passive else BluetoothScanningMode.ACTIVE + scanner = HaScanner(hass, mode, adapter, address) try: - bleak_scanner = create_bleak_scanner(mode, adapter) + scanner.async_setup() except RuntimeError as err: raise ConfigEntryNotReady( f"{adapter_human_name(adapter, address)}: {err}" ) from err - scanner = HaScanner(hass, bleak_scanner, adapter, address) info_callback = async_get_advertisement_callback(hass) entry.async_on_unload(scanner.async_register_callback(info_callback)) try: diff --git a/homeassistant/components/bluetooth/active_update_coordinator.py b/homeassistant/components/bluetooth/active_update_coordinator.py index b207f6fa2e1..37f049d3e07 100644 --- a/homeassistant/components/bluetooth/active_update_coordinator.py +++ b/homeassistant/components/bluetooth/active_update_coordinator.py @@ -117,12 +117,12 @@ class ActiveBluetoothProcessorCoordinator( "%s: Bluetooth error whilst polling: %s", self.address, str(exc) ) self.last_poll_successful = False - return + return except Exception: # pylint: disable=broad-except if self.last_poll_successful: self.logger.exception("%s: Failure while polling", self.address) self.last_poll_successful = False - return + return finally: self._last_poll = time.monotonic() diff --git a/homeassistant/components/bluetooth/const.py b/homeassistant/components/bluetooth/const.py index 891e6d8be82..4d4a096bb66 100644 --- a/homeassistant/components/bluetooth/const.py +++ b/homeassistant/components/bluetooth/const.py @@ -66,3 +66,6 @@ ADAPTER_ADDRESS: Final = "address" ADAPTER_SW_VERSION: Final = "sw_version" ADAPTER_HW_VERSION: Final = "hw_version" ADAPTER_PASSIVE_SCAN: Final = "passive_scan" + + +NO_RSSI_VALUE: Final = -1000 diff --git a/homeassistant/components/bluetooth/manager.py b/homeassistant/components/bluetooth/manager.py index 9f6b0bbe3ed..37c24423231 100644 --- a/homeassistant/components/bluetooth/manager.py +++ b/homeassistant/components/bluetooth/manager.py @@ -24,6 +24,7 @@ from homeassistant.helpers.event import async_track_time_interval from .const import ( ADAPTER_ADDRESS, ADAPTER_PASSIVE_SCAN, + NO_RSSI_VALUE, STALE_ADVERTISEMENT_SECONDS, UNAVAILABLE_TRACK_SECONDS, AdapterDetails, @@ -45,7 +46,7 @@ from .models import ( BluetoothServiceInfoBleak, ) from .usage import install_multiple_bleak_catcher, uninstall_multiple_bleak_catcher -from .util import async_get_bluetooth_adapters +from .util import async_get_bluetooth_adapters, async_load_history_from_system if TYPE_CHECKING: from bleak.backends.device import BLEDevice @@ -55,12 +56,16 @@ if TYPE_CHECKING: FILTER_UUIDS: Final = "UUIDs" APPLE_MFR_ID: Final = 76 +APPLE_IBEACON_START_BYTE: Final = 0x02 # iBeacon (tilt_ble) APPLE_HOMEKIT_START_BYTE: Final = 0x06 # homekit_controller APPLE_DEVICE_ID_START_BYTE: Final = 0x10 # bluetooth_le_tracker -APPLE_START_BYTES_WANTED: Final = {APPLE_DEVICE_ID_START_BYTE, APPLE_HOMEKIT_START_BYTE} +APPLE_START_BYTES_WANTED: Final = { + APPLE_IBEACON_START_BYTE, + APPLE_HOMEKIT_START_BYTE, + APPLE_DEVICE_ID_START_BYTE, +} RSSI_SWITCH_THRESHOLD = 6 -NO_RSSI_VALUE = -1000 _LOGGER = logging.getLogger(__name__) @@ -106,7 +111,7 @@ def _prefer_previous_adv( def _dispatch_bleak_callback( - callback: AdvertisementDataCallback, + callback: AdvertisementDataCallback | None, filters: dict[str, set[str]], device: BLEDevice, advertisement_data: AdvertisementData, @@ -114,7 +119,7 @@ def _dispatch_bleak_callback( """Dispatch the callback.""" if not callback: # Callback destroyed right before being called, ignore - return # type: ignore[unreachable] # pragma: no cover + return # pragma: no cover if (uuids := filters.get(FILTER_UUIDS)) and not uuids.intersection( advertisement_data.service_uuids @@ -139,9 +144,11 @@ class BluetoothManager: self.hass = hass self._integration_matcher = integration_matcher self._cancel_unavailable_tracking: list[CALLBACK_TYPE] = [] - self._unavailable_callbacks: dict[str, list[Callable[[str], None]]] = {} + self._unavailable_callbacks: dict[ + str, list[Callable[[BluetoothServiceInfoBleak], None]] + ] = {} self._connectable_unavailable_callbacks: dict[ - str, list[Callable[[str], None]] + str, list[Callable[[BluetoothServiceInfoBleak], None]] ] = {} self._callback_index = BluetoothCallbackMatcherIndex() self._bleak_callbacks: list[ @@ -207,10 +214,15 @@ class BluetoothManager: self._adapters = await async_get_bluetooth_adapters() return self._find_adapter_by_address(address) - @hass_callback - def async_setup(self) -> None: + async def async_setup(self) -> None: """Set up the bluetooth manager.""" install_multiple_bleak_catcher() + history = await async_load_history_from_system() + # Everything is connectable so it fall into both + # buckets since the host system can only provide + # connectable devices + self._history = history.copy() + self._connectable_history = history.copy() self.async_setup_unavailable_tracking() @hass_callback @@ -223,6 +235,23 @@ class BluetoothManager: self._cancel_unavailable_tracking.clear() uninstall_multiple_bleak_catcher() + async def async_get_devices_by_address( + self, address: str, connectable: bool + ) -> list[BLEDevice]: + """Get devices by address.""" + types_ = (True,) if connectable else (True, False) + return [ + device + for device in await asyncio.gather( + *( + scanner.async_get_device_by_address(address) + for type_ in types_ + for scanner in self._get_scanners_by_type(type_) + ) + ) + if device is not None + ] + @hass_callback def async_all_discovered_devices(self, connectable: bool) -> Iterable[BLEDevice]: """Return all of discovered devices from all the scanners including duplicates.""" @@ -265,12 +294,12 @@ class BluetoothManager: } disappeared = history_set.difference(active_addresses) for address in disappeared: - del history[address] + service_info = history.pop(address) if not (callbacks := unavailable_callbacks.get(address)): continue for callback in callbacks: try: - callback(address) + callback(service_info) except Exception: # pylint: disable=broad-except _LOGGER.exception("Error in unavailable callback") @@ -317,12 +346,24 @@ class BluetoothManager: return self._history[address] = service_info - source = service_info.source if connectable: self._connectable_history[address] = service_info # Bleak callbacks must get a connectable device + # If the advertisement data is the same as the last time we saw it, we + # don't need to do anything else. + if old_service_info and not ( + service_info.manufacturer_data != old_service_info.manufacturer_data + or service_info.service_data != old_service_info.service_data + or service_info.service_uuids != old_service_info.service_uuids + or service_info.name != old_service_info.name + ): + return + + source = service_info.source + if connectable: + # Bleak callbacks must get a connectable device for callback_filters in self._bleak_callbacks: _dispatch_bleak_callback(*callback_filters, device, advertisement_data) @@ -354,7 +395,10 @@ class BluetoothManager: @hass_callback def async_track_unavailable( - self, callback: Callable[[str], None], address: str, connectable: bool + self, + callback: Callable[[BluetoothServiceInfoBleak], None], + address: str, + connectable: bool, ) -> Callable[[], None]: """Register a callback.""" unavailable_callbacks = self._get_unavailable_callbacks_by_type(connectable) @@ -426,9 +470,16 @@ class BluetoothManager: def async_discovered_service_info( self, connectable: bool ) -> Iterable[BluetoothServiceInfoBleak]: - """Return if the address is present.""" + """Return all the discovered services info.""" return self._get_history_by_type(connectable).values() + @hass_callback + def async_last_service_info( + self, address: str, connectable: bool + ) -> BluetoothServiceInfoBleak | None: + """Return the last service info for an address.""" + return self._get_history_by_type(connectable).get(address) + @hass_callback def async_rediscover_address(self, address: str) -> None: """Trigger discovery of devices which have already been seen.""" @@ -444,7 +495,7 @@ class BluetoothManager: def _get_unavailable_callbacks_by_type( self, connectable: bool - ) -> dict[str, list[Callable[[str], None]]]: + ) -> dict[str, list[Callable[[BluetoothServiceInfoBleak], None]]]: """Return the unavailable callbacks by type.""" return ( self._connectable_unavailable_callbacks diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 413ceb77e39..f81e1324da4 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -3,13 +3,14 @@ "name": "Bluetooth", "documentation": "https://www.home-assistant.io/integrations/bluetooth", "dependencies": ["usb"], + "after_dependencies": ["hassio"], "quality_scale": "internal", "requirements": [ - "bleak==0.17.0", - "bleak-retry-connector==1.17.1", - "bluetooth-adapters==0.4.1", + "bleak==0.18.1", + "bleak-retry-connector==2.1.3", + "bluetooth-adapters==0.6.0", "bluetooth-auto-recovery==0.3.3", - "dbus-fast==1.5.1" + "dbus-fast==1.24.0" ], "codeowners": ["@bdraco"], "config_flow": true, diff --git a/homeassistant/components/bluetooth/models.py b/homeassistant/components/bluetooth/models.py index 6c70633f597..d93f8efc1e2 100644 --- a/homeassistant/components/bluetooth/models.py +++ b/homeassistant/components/bluetooth/models.py @@ -11,17 +11,21 @@ import logging 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_retry_connector import freshen_ble_device -from homeassistant.core import CALLBACK_TYPE +from homeassistant.core import CALLBACK_TYPE, callback as hass_callback from homeassistant.helpers.frame import report from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo +from .const import NO_RSSI_VALUE + if TYPE_CHECKING: from .manager import BluetoothManager @@ -62,6 +66,23 @@ BluetoothCallback = Callable[[BluetoothServiceInfoBleak, BluetoothChange], None] ProcessAdvertisementCallback = Callable[[BluetoothServiceInfoBleak], bool] +@dataclass +class HaBluetoothConnector: + """Data for how to connect a BLEDevice from a given scanner.""" + + client: type[BaseBleakClient] + source: str + can_connect: Callable[[], bool] + + +@dataclass +class _HaWrappedBleakBackend: + """Wrap bleak backend to make it usable by Home Assistant.""" + + device: BLEDevice + client: type[BaseBleakClient] + + class BaseHaScanner: """Base class for Ha Scanners.""" @@ -70,6 +91,10 @@ class BaseHaScanner: def discovered_devices(self) -> list[BLEDevice]: """Return a list of discovered devices.""" + @abstractmethod + async def async_get_device_by_address(self, address: str) -> BLEDevice | None: + """Get a device by address.""" + async def async_diagnostics(self) -> dict[str, Any]: """Return diagnostic information about the scanner.""" return { @@ -109,6 +134,12 @@ class HaBleakScannerWrapper(BaseBleakScanner): detection_callback=detection_callback, service_uuids=service_uuids or [] ) + @classmethod + async def discover(cls, timeout: float = 5.0, **kwargs: Any) -> list[BLEDevice]: + """Discover devices.""" + assert MANAGER is not None + return list(MANAGER.async_discovered_devices(True)) + async def stop(self, *args: Any, **kwargs: Any) -> None: """Stop scanning for devices.""" @@ -189,20 +220,117 @@ class HaBleakClientWrapper(BleakClient): when an integration does this. """ - def __init__( - self, address_or_ble_device: str | BLEDevice, *args: Any, **kwargs: Any + def __init__( # pylint: disable=super-init-not-called, keyword-arg-before-vararg + self, + address_or_ble_device: str | BLEDevice, + disconnected_callback: Callable[[BleakClient], None] | None = None, + *args: Any, + timeout: float = 10.0, + **kwargs: Any, ) -> None: """Initialize the BleakClient.""" if isinstance(address_or_ble_device, BLEDevice): - super().__init__(address_or_ble_device, *args, **kwargs) - return - report( - "attempted to call BleakClient with an address instead of a BLEDevice", - exclude_integrations={"bluetooth"}, - error_if_core=False, - ) + self.__address = address_or_ble_device.address + else: + report( + "attempted to call BleakClient with an address instead of a BLEDevice", + exclude_integrations={"bluetooth"}, + error_if_core=False, + ) + self.__address = address_or_ble_device + self.__disconnected_callback = disconnected_callback + self.__timeout = timeout + self._backend: BaseBleakClient | None = None # type: ignore[assignment] + + @property + def is_connected(self) -> bool: + """Return True if the client is connected to a device.""" + return self._backend is not None and self._backend.is_connected + + def set_disconnected_callback( + self, + callback: Callable[[BleakClient], None] | None, + **kwargs: Any, + ) -> None: + """Set the disconnect callback.""" + self.__disconnected_callback = callback + if self._backend: + self._backend.set_disconnected_callback(callback, **kwargs) # type: ignore[arg-type] + + async def connect(self, **kwargs: Any) -> bool: + """Connect to the specified GATT server.""" + if not self._backend: + wrapped_backend = ( + self._async_get_backend() or await self._async_get_fallback_backend() + ) + self._backend = wrapped_backend.client( + await freshen_ble_device(wrapped_backend.device) + or wrapped_backend.device, + disconnected_callback=self.__disconnected_callback, + timeout=self.__timeout, + ) + return await super().connect(**kwargs) + + @hass_callback + def _async_get_backend_for_ble_device( + self, ble_device: BLEDevice + ) -> _HaWrappedBleakBackend | None: + """Get the backend for a BLEDevice.""" + details = ble_device.details + if not isinstance(details, dict) or "connector" not in details: + # If client is not defined in details + # its the client for this platform + cls = get_platform_client_backend_type() + return _HaWrappedBleakBackend(ble_device, cls) + + connector: HaBluetoothConnector = details["connector"] + # Make sure the backend can connect to the device + # as some backends have connection limits + if not connector.can_connect(): + return None + + return _HaWrappedBleakBackend(ble_device, connector.client) + + @hass_callback + def _async_get_backend(self) -> _HaWrappedBleakBackend | None: + """Get the bleak backend for the given address.""" assert MANAGER is not None - ble_device = MANAGER.async_ble_device_from_address(address_or_ble_device, True) + address = self.__address + ble_device = MANAGER.async_ble_device_from_address(address, True) if ble_device is None: - raise BleakError(f"No device found for address {address_or_ble_device}") - super().__init__(ble_device, *args, **kwargs) + raise BleakError(f"No device found for address {address}") + + if backend := self._async_get_backend_for_ble_device(ble_device): + return backend + + return None + + async def _async_get_fallback_backend(self) -> _HaWrappedBleakBackend: + """Get a fallback backend for the given address.""" + # + # The preferred backend cannot currently connect the device + # because it is likely out of connection slots. + # + # We need to try all backends to find one that can + # connect to the device. + # + assert MANAGER is not None + address = self.__address + devices = await MANAGER.async_get_devices_by_address(address, True) + for ble_device in sorted( + devices, + key=lambda ble_device: ble_device.rssi or NO_RSSI_VALUE, + reverse=True, + ): + if backend := self._async_get_backend_for_ble_device(ble_device): + return backend + + raise BleakError( + f"No backend with an available connection slot that can reach address {address} was found" + ) + + async def disconnect(self) -> bool: + """Disconnect from the device.""" + if self._backend is None: + return True + return await self._backend.disconnect() diff --git a/homeassistant/components/bluetooth/passive_update_coordinator.py b/homeassistant/components/bluetooth/passive_update_coordinator.py index 296e49e2fa0..1eae49a6cab 100644 --- a/homeassistant/components/bluetooth/passive_update_coordinator.py +++ b/homeassistant/components/bluetooth/passive_update_coordinator.py @@ -41,9 +41,11 @@ class PassiveBluetoothDataUpdateCoordinator(BasePassiveBluetoothCoordinator): update_callback() @callback - def _async_handle_unavailable(self, address: str) -> None: + def _async_handle_unavailable( + self, service_info: BluetoothServiceInfoBleak + ) -> None: """Handle the device going unavailable.""" - super()._async_handle_unavailable(address) + super()._async_handle_unavailable(service_info) self.async_update_listeners() @callback @@ -73,7 +75,6 @@ class PassiveBluetoothDataUpdateCoordinator(BasePassiveBluetoothCoordinator): change: BluetoothChange, ) -> None: """Handle a Bluetooth event.""" - super()._async_handle_bluetooth_event(service_info, change) self.async_update_listeners() diff --git a/homeassistant/components/bluetooth/passive_update_processor.py b/homeassistant/components/bluetooth/passive_update_processor.py index 5ea2f3f0742..b04447cc4ee 100644 --- a/homeassistant/components/bluetooth/passive_update_processor.py +++ b/homeassistant/components/bluetooth/passive_update_processor.py @@ -101,9 +101,11 @@ class PassiveBluetoothProcessorCoordinator( return remove_processor @callback - def _async_handle_unavailable(self, address: str) -> None: + def _async_handle_unavailable( + self, service_info: BluetoothServiceInfoBleak + ) -> None: """Handle the device going unavailable.""" - super()._async_handle_unavailable(address) + super()._async_handle_unavailable(service_info) for processor in self._processors: processor.async_handle_unavailable() diff --git a/homeassistant/components/bluetooth/scanner.py b/homeassistant/components/bluetooth/scanner.py index 184e8775f07..9bc68059a7f 100644 --- a/homeassistant/components/bluetooth/scanner.py +++ b/homeassistant/components/bluetooth/scanner.py @@ -16,21 +16,17 @@ from bleak.assigned_numbers import AdvertisementDataType from bleak.backends.bluezdbus.advertisement_monitor import OrPattern from bleak.backends.bluezdbus.scanner import BlueZScannerArgs from bleak.backends.device import BLEDevice -from bleak.backends.scanner import AdvertisementData +from bleak.backends.scanner import AdvertisementData, AdvertisementDataCallback +from bleak_retry_connector import get_device_by_adapter from dbus_fast import InvalidMessageError -from homeassistant.const import EVENT_HOMEASSISTANT_STOP -from homeassistant.core import ( - CALLBACK_TYPE, - Event, - HomeAssistant, - callback as hass_callback, -) +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback as hass_callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.event import async_track_time_interval from homeassistant.util.package import is_docker_env from .const import ( + DEFAULT_ADDRESS, SCANNER_WATCHDOG_INTERVAL, SCANNER_WATCHDOG_TIMEOUT, SOURCE_LOCAL, @@ -90,11 +86,14 @@ class ScannerStartError(HomeAssistantError): def create_bleak_scanner( - scanning_mode: BluetoothScanningMode, adapter: str | None + detection_callback: AdvertisementDataCallback, + scanning_mode: BluetoothScanningMode, + adapter: str | None, ) -> bleak.BleakScanner: """Create a Bleak scanner.""" scanner_kwargs: dict[str, Any] = { - "scanning_mode": SCANNING_MODE_TO_BLEAK[scanning_mode] + "detection_callback": detection_callback, + "scanning_mode": SCANNING_MODE_TO_BLEAK[scanning_mode], } if platform.system() == "Linux": # Only Linux supports multiple adapters @@ -121,31 +120,49 @@ class HaScanner(BaseHaScanner): over ethernet, usb over ethernet, etc. """ + scanner: bleak.BleakScanner + def __init__( self, hass: HomeAssistant, - scanner: bleak.BleakScanner, + mode: BluetoothScanningMode, adapter: str, address: str, ) -> None: """Init bluetooth discovery.""" self.hass = hass - self.scanner = scanner + self.mode = mode self.adapter = adapter self._start_stop_lock = asyncio.Lock() - self._cancel_stop: CALLBACK_TYPE | None = None self._cancel_watchdog: CALLBACK_TYPE | None = None self._last_detection = 0.0 self._start_time = 0.0 self._callbacks: list[Callable[[BluetoothServiceInfoBleak], None]] = [] self.name = adapter_human_name(adapter, address) - self.source = self.adapter or SOURCE_LOCAL + self.source = address if address != DEFAULT_ADDRESS else adapter or SOURCE_LOCAL @property def discovered_devices(self) -> list[BLEDevice]: """Return a list of discovered devices.""" return self.scanner.discovered_devices + @hass_callback + def async_setup(self) -> None: + """Set up the scanner.""" + self.scanner = create_bleak_scanner( + self._async_detection_callback, self.mode, self.adapter + ) + + async def async_get_device_by_address(self, address: str) -> BLEDevice | None: + """Get a device by address.""" + if platform.system() == "Linux": + return await get_device_by_adapter(address, self.adapter) + # We don't have a fast version of this for MacOS yet + return next( + (device for device in self.discovered_devices if device.address == address), + None, + ) + async def async_diagnostics(self) -> dict[str, Any]: """Return diagnostic information about the scanner.""" base_diag = await super().async_diagnostics() @@ -213,8 +230,6 @@ class HaScanner(BaseHaScanner): async def async_start(self) -> None: """Start bluetooth scanner.""" - self.scanner.register_detection_callback(self._async_detection_callback) - async with self._start_stop_lock: await self._async_start() @@ -318,9 +333,6 @@ class HaScanner(BaseHaScanner): break self._async_setup_scanner_watchdog() - self._cancel_stop = self.hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, self._async_hass_stopping - ) @hass_callback def _async_setup_scanner_watchdog(self) -> None: @@ -368,11 +380,6 @@ class HaScanner(BaseHaScanner): exc_info=True, ) - async def _async_hass_stopping(self, event: Event) -> None: - """Stop the Bluetooth integration at shutdown.""" - self._cancel_stop = None - await self.async_stop() - async def _async_reset_adapter(self) -> None: """Reset the adapter.""" # There is currently nothing the user can do to fix this @@ -396,9 +403,6 @@ class HaScanner(BaseHaScanner): async def _async_stop_scanner(self) -> None: """Stop bluetooth discovery under the lock.""" - if self._cancel_stop: - self._cancel_stop() - self._cancel_stop = None _LOGGER.debug("%s: Stopping bluetooth discovery", self.name) try: await self.scanner.stop() # type: ignore[no-untyped-call] diff --git a/homeassistant/components/bluetooth/strings.json b/homeassistant/components/bluetooth/strings.json index f838cd97798..cfde1b90cd8 100644 --- a/homeassistant/components/bluetooth/strings.json +++ b/homeassistant/components/bluetooth/strings.json @@ -1,4 +1,10 @@ { + "issues": { + "haos_outdated": { + "title": "Update to Home Assistant Operating System 9.0 or later", + "description": "To improve Bluetooth reliability and performance, we highly recommend you update to version 9.0 or later of the Home Assistant Operating System." + } + }, "config": { "flow_title": "{name}", "step": { diff --git a/homeassistant/components/bluetooth/translations/bg.json b/homeassistant/components/bluetooth/translations/bg.json new file mode 100644 index 00000000000..7da387cae4c --- /dev/null +++ b/homeassistant/components/bluetooth/translations/bg.json @@ -0,0 +1,47 @@ +{ + "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", + "no_adapters": "\u041d\u044f\u043c\u0430 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 \u043d\u0435\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0438 Bluetooth \u0430\u0434\u0430\u043f\u0442\u0435\u0440\u0438" + }, + "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}?" + }, + "enable_bluetooth": { + "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 Bluetooth?" + }, + "multiple_adapters": { + "data": { + "adapter": "\u0410\u0434\u0430\u043f\u0442\u0435\u0440" + }, + "description": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 Bluetooth \u0430\u0434\u0430\u043f\u0442\u0435\u0440 \u0437\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430" + }, + "single_adapter": { + "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 Bluetooth \u0430\u0434\u0430\u043f\u0442\u0435\u0440\u0430 {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" + } + } + }, + "issues": { + "haos_outdated": { + "title": "\u0410\u043a\u0442\u0443\u0430\u043b\u0438\u0437\u0438\u0440\u0430\u0439\u0442\u0435 \u0434\u043e \u043e\u043f\u0435\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u0430 \u0441\u0438\u0441\u0442\u0435\u043c\u0430 Home Assistant 9.0 \u0438\u043b\u0438 \u043f\u043e-\u043d\u043e\u0432\u0430" + } + }, + "options": { + "step": { + "init": { + "data": { + "adapter": "Bluetooth \u0430\u0434\u0430\u043f\u0442\u0435\u0440, \u043a\u043e\u0439\u0442\u043e \u0434\u0430 \u0441\u0435 \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430 \u0437\u0430 \u0441\u043a\u0430\u043d\u0438\u0440\u0430\u043d\u0435", + "passive": "\u041f\u0430\u0441\u0438\u0432\u043d\u043e \u0441\u043a\u0430\u043d\u0438\u0440\u0430\u043d\u0435" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bluetooth/translations/ca.json b/homeassistant/components/bluetooth/translations/ca.json index 6c1554dc0d9..8c124446672 100644 --- a/homeassistant/components/bluetooth/translations/ca.json +++ b/homeassistant/components/bluetooth/translations/ca.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "El servei ja est\u00e0 configurat", - "no_adapters": "No s'ha trobat cap adaptador Bluetooth" + "no_adapters": "No s'han trobat adaptadors Bluetooth sense configurar" }, "flow_title": "{name}", "step": { @@ -29,13 +29,20 @@ } } }, + "issues": { + "haos_outdated": { + "description": "Per millorar la fiabilitat i el rendiment de Bluetooth, et recomanem que actualitzis a la versi\u00f3 9.0 o posterior del sistema operatiu Home Assistant.", + "title": "Actualitza el sistema operatiu a Home Assistant 9.0 o posterior" + } + }, "options": { "step": { "init": { "data": { "adapter": "Adaptador Bluetooth a utilitzar per escanejar", - "passive": "Escolta passiva" - } + "passive": "Escaneig passiu" + }, + "description": "L'escolta passiva necessita BlueZ 5.63 o posterior i les funcions experimentals activades." } } } diff --git a/homeassistant/components/bluetooth/translations/cs.json b/homeassistant/components/bluetooth/translations/cs.json new file mode 100644 index 00000000000..e53690d3458 --- /dev/null +++ b/homeassistant/components/bluetooth/translations/cs.json @@ -0,0 +1,22 @@ +{ + "config": { + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Chcete nastavit {name}?" + }, + "enable_bluetooth": { + "description": "Chcete nastavit Bluetooth?" + }, + "single_adapter": { + "description": "Chcete nastavit Bluetooth adapt\u00e9r {name}?" + }, + "user": { + "data": { + "address": "Za\u0159\u00edzen\u00ed" + }, + "description": "Zvolte za\u0159\u00edzen\u00ed, kter\u00e9 chcete nastavit" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bluetooth/translations/de.json b/homeassistant/components/bluetooth/translations/de.json index 1f8f48e05ee..63bbf51c59e 100644 --- a/homeassistant/components/bluetooth/translations/de.json +++ b/homeassistant/components/bluetooth/translations/de.json @@ -29,6 +29,12 @@ } } }, + "issues": { + "haos_outdated": { + "description": "Zur Verbesserung der Bluetooth-Zuverl\u00e4ssigkeit und -Leistung empfehlen wir dir dringend ein Update auf Version 9.0 oder h\u00f6her des Home Assistant-Betriebssystems.", + "title": "Aktualisiere auf das Home Assistant-Betriebssystem 9.0 oder h\u00f6her" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/bluetooth/translations/el.json b/homeassistant/components/bluetooth/translations/el.json index 1d6685d307e..f31e930b3fa 100644 --- a/homeassistant/components/bluetooth/translations/el.json +++ b/homeassistant/components/bluetooth/translations/el.json @@ -29,6 +29,12 @@ } } }, + "issues": { + "haos_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\u03b1\u03c2 \u03c3\u03c5\u03bd\u03b9\u03c3\u03c4\u03bf\u03cd\u03bc\u03b5 \u03b1\u03bd\u03b5\u03c0\u03b9\u03c6\u03cd\u03bb\u03b1\u03ba\u03c4\u03b1 \u03bd\u03b1 \u03b5\u03bd\u03b7\u03bc\u03b5\u03c1\u03ce\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7\u03bd \u03ad\u03ba\u03b4\u03bf\u03c3\u03b7 9.0 \u03ae \u03bd\u03b5\u03cc\u03c4\u03b5\u03c1\u03b7 \u03c4\u03bf\u03c5 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03b9\u03ba\u03bf\u03cd \u03c3\u03c5\u03c3\u03c4\u03ae\u03bc\u03b1\u03c4\u03bf\u03c2 Home Assistant.", + "title": "\u0395\u03bd\u03b7\u03bc\u03ad\u03c1\u03c9\u03c3\u03b7 \u03c3\u03b5 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03b9\u03ba\u03cc \u03c3\u03cd\u03c3\u03c4\u03b7\u03bc\u03b1 Home Assistant 9.0 \u03ae \u03bd\u03b5\u03cc\u03c4\u03b5\u03c1\u03b7 \u03ad\u03ba\u03b4\u03bf\u03c3\u03b7" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/bluetooth/translations/en.json b/homeassistant/components/bluetooth/translations/en.json index 7d76740602d..73ed74356fd 100644 --- a/homeassistant/components/bluetooth/translations/en.json +++ b/homeassistant/components/bluetooth/translations/en.json @@ -29,6 +29,12 @@ } } }, + "issues": { + "haos_outdated": { + "description": "To improve Bluetooth reliability and performance, we highly recommend you update to version 9.0 or later of the Home Assistant Operating System.", + "title": "Update to Home Assistant Operating System 9.0 or later" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/bluetooth/translations/es.json b/homeassistant/components/bluetooth/translations/es.json index b28fc6cc695..4cfa38df9c2 100644 --- a/homeassistant/components/bluetooth/translations/es.json +++ b/homeassistant/components/bluetooth/translations/es.json @@ -29,6 +29,12 @@ } } }, + "issues": { + "haos_outdated": { + "description": "Para mejorar la confiabilidad y el rendimiento de Bluetooth, te recomendamos que actualices a la versi\u00f3n 9.0 o posterior de Home Assistant Operating System.", + "title": "Actualiza a Home Assistant Operating System 9.0 o posterior" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/bluetooth/translations/he.json b/homeassistant/components/bluetooth/translations/he.json index b5740956a9d..ad9bb727c62 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" + "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" }, "flow_title": "{name}", "step": { @@ -12,6 +12,15 @@ "enable_bluetooth": { "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05e9\u05df \u05db\u05d7\u05d5\u05dc\u05d4?" }, + "multiple_adapters": { + "data": { + "adapter": "\u05de\u05ea\u05d0\u05dd" + }, + "description": "\u05d1\u05d7\u05d9\u05e8\u05ea \u05de\u05ea\u05d0\u05dd \u05e9\u05df \u05db\u05d7\u05d5\u05dc\u05d4 \u05dc\u05d4\u05ea\u05e7\u05e0\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}?" + }, "user": { "data": { "address": "\u05d4\u05ea\u05e7\u05df" @@ -24,8 +33,10 @@ "step": { "init": { "data": { - "adapter": "\u05de\u05ea\u05d0\u05dd \u05d4\u05e9\u05df \u05d4\u05db\u05d7\u05d5\u05dc\u05d4 \u05dc\u05e9\u05d9\u05de\u05d5\u05e9 \u05dc\u05e1\u05e8\u05d9\u05e7\u05d4" - } + "adapter": "\u05de\u05ea\u05d0\u05dd \u05d4\u05e9\u05df \u05d4\u05db\u05d7\u05d5\u05dc\u05d4 \u05dc\u05e9\u05d9\u05de\u05d5\u05e9 \u05dc\u05e1\u05e8\u05d9\u05e7\u05d4", + "passive": "\u05e1\u05e8\u05d9\u05e7\u05d4 \u05e4\u05e1\u05d9\u05d1\u05d9\u05ea" + }, + "description": "\u05d4\u05d0\u05d6\u05e0\u05d4 \u05e4\u05e1\u05d9\u05d1\u05d9\u05ea \u05d3\u05d5\u05e8\u05e9\u05ea BlueZ 5.63 \u05d5\u05d0\u05d9\u05dc\u05da \u05e2\u05dd \u05ea\u05db\u05d5\u05e0\u05d5\u05ea \u05e0\u05d9\u05e1\u05d9\u05d5\u05e0\u05d9\u05d5\u05ea \u05de\u05d5\u05e4\u05e2\u05dc\u05d5\u05ea." } } } diff --git a/homeassistant/components/bluetooth/translations/hu.json b/homeassistant/components/bluetooth/translations/hu.json index a79bac619d8..79dc3204031 100644 --- a/homeassistant/components/bluetooth/translations/hu.json +++ b/homeassistant/components/bluetooth/translations/hu.json @@ -29,12 +29,18 @@ } } }, + "issues": { + "haos_outdated": { + "description": "A Bluetooth megb\u00edzhat\u00f3s\u00e1g\u00e1nak \u00e9s teljes\u00edtm\u00e9ny\u00e9nek jav\u00edt\u00e1sa \u00e9rdek\u00e9ben er\u0151sen javasoljuk, hogy friss\u00edtse a Home Assistant oper\u00e1ci\u00f3s rendszer\u00e9t 9.0 vagy \u00fajabb verzi\u00f3ra.", + "title": "Home Assistant oper\u00e1ci\u00f3s rendszer\u00e9nek friss\u00edt\u00e9se 9.0 vagy \u00fajabb verzi\u00f3ra" + } + }, "options": { "step": { "init": { "data": { "adapter": "A szkennel\u00e9shez haszn\u00e1lhat\u00f3 Bluetooth-adapter", - "passive": "Passz\u00edv hallgat\u00e1s" + "passive": "Passz\u00edv figyel\u00e9s" }, "description": "A passz\u00edv hallgat\u00e1shoz BlueZ 5.63 vagy \u00fajabb verzi\u00f3ra van sz\u00fcks\u00e9g, a k\u00eds\u00e9rleti funkci\u00f3k enged\u00e9lyez\u00e9s\u00e9vel." } diff --git a/homeassistant/components/bluetooth/translations/id.json b/homeassistant/components/bluetooth/translations/id.json index c74420cd281..282071fc4f1 100644 --- a/homeassistant/components/bluetooth/translations/id.json +++ b/homeassistant/components/bluetooth/translations/id.json @@ -29,6 +29,12 @@ } } }, + "issues": { + "haos_outdated": { + "description": "Untuk meningkatkan keandalan dan performa Bluetooth, kami sangat menyarankan Anda memperbarui ke versi 9.0 atau yang lebih baru dari Home Assistant Operating System.", + "title": "Perbarui ke Home Assistant Operating System 9.0 atau yang lebih baru" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/bluetooth/translations/it.json b/homeassistant/components/bluetooth/translations/it.json index fc9ea431d29..6d3adac2f76 100644 --- a/homeassistant/components/bluetooth/translations/it.json +++ b/homeassistant/components/bluetooth/translations/it.json @@ -29,6 +29,12 @@ } } }, + "issues": { + "haos_outdated": { + "description": "Per migliorare l'affidabilit\u00e0 e le prestazioni del Bluetooth, si consiglia vivamente di aggiornare alla versione 9.0 o successiva del sistema operativo Home Assistant.", + "title": "Aggiorna al Home Assistant OS 9.0 o successivo" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/bluetooth/translations/ja.json b/homeassistant/components/bluetooth/translations/ja.json index ea90f827e41..e19ee5b7dd2 100644 --- a/homeassistant/components/bluetooth/translations/ja.json +++ b/homeassistant/components/bluetooth/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u30b5\u30fc\u30d3\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", - "no_adapters": "Bluetooth\u30a2\u30c0\u30d7\u30bf\u30fc\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093" + "no_adapters": "\u672a\u69cb\u6210\u306e Bluetooth \u30a2\u30c0\u30d7\u30bf\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/bluetooth/translations/nl.json b/homeassistant/components/bluetooth/translations/nl.json index 9a0e95df8bc..c9db452612e 100644 --- a/homeassistant/components/bluetooth/translations/nl.json +++ b/homeassistant/components/bluetooth/translations/nl.json @@ -20,6 +20,11 @@ } } }, + "issues": { + "haos_outdated": { + "title": "Update naar het Home Assistant-besturingssysteem versie 9.0 of hoger" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/bluetooth/translations/no.json b/homeassistant/components/bluetooth/translations/no.json index 5ab1050a849..687b651eaca 100644 --- a/homeassistant/components/bluetooth/translations/no.json +++ b/homeassistant/components/bluetooth/translations/no.json @@ -29,6 +29,12 @@ } } }, + "issues": { + "haos_outdated": { + "description": "For \u00e5 forbedre Bluetooth-p\u00e5litelighet og ytelse, anbefaler vi p\u00e5 det sterkeste at du oppdaterer til versjon 9.0 eller nyere av Home Assistant-operativsystemet.", + "title": "Oppdater til Home Assistant-operativsystem 9.0 eller nyere" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/bluetooth/translations/pl.json b/homeassistant/components/bluetooth/translations/pl.json index 9c52b5a136f..2de99ab69fe 100644 --- a/homeassistant/components/bluetooth/translations/pl.json +++ b/homeassistant/components/bluetooth/translations/pl.json @@ -12,6 +12,15 @@ "enable_bluetooth": { "description": "Czy chcesz skonfigurowa\u0107 Bluetooth?" }, + "multiple_adapters": { + "data": { + "adapter": "Adapter" + }, + "description": "Wybierz adapter Bluetooth do konfiguracji" + }, + "single_adapter": { + "description": "Czy chcesz skonfigurowa\u0107 adapter Bluetooth {name}?" + }, "user": { "data": { "address": "Urz\u0105dzenie" @@ -20,13 +29,20 @@ } } }, + "issues": { + "haos_outdated": { + "description": "Aby poprawi\u0107 niezawodno\u015b\u0107 i wydajno\u015b\u0107 Bluetooth, zdecydowanie zalecamy aktualizacj\u0119 Home Assistant OS do wersji 9.0 lub nowszej.", + "title": "Aktualizacja Home Assistant OS do wersji 9.0 lub nowszej" + } + }, "options": { "step": { "init": { "data": { "adapter": "Adapter Bluetooth u\u017cywany do skanowania", "passive": "Skanowanie pasywne" - } + }, + "description": "Nas\u0142uchiwanie pasywne wymaga BlueZ 5.63 lub nowszego z w\u0142\u0105czonymi funkcjami eksperymentalnymi." } } } diff --git a/homeassistant/components/bluetooth/translations/pt-BR.json b/homeassistant/components/bluetooth/translations/pt-BR.json index 11c802b5023..389205c20db 100644 --- a/homeassistant/components/bluetooth/translations/pt-BR.json +++ b/homeassistant/components/bluetooth/translations/pt-BR.json @@ -29,6 +29,12 @@ } } }, + "issues": { + "haos_outdated": { + "description": "Para melhorar a confiabilidade e o desempenho do Bluetooth, recomendamos que voc\u00ea atualize para a vers\u00e3o 9.0 ou posterior do sistema operacional Home Assistant.", + "title": "Atualiza\u00e7\u00e3o para o sistema operacional Home Assistant 9.0 ou posterior" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/bluetooth/translations/ru.json b/homeassistant/components/bluetooth/translations/ru.json index 3270f8b840d..1be63589bb4 100644 --- a/homeassistant/components/bluetooth/translations/ru.json +++ b/homeassistant/components/bluetooth/translations/ru.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u042d\u0442\u0430 \u0441\u043b\u0443\u0436\u0431\u0430 \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.", - "no_adapters": "\u0410\u0434\u0430\u043f\u0442\u0435\u0440\u044b Bluetooth \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b." + "no_adapters": "\u041d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u043e \u043d\u0435\u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u043d\u044b\u0445 \u0430\u0434\u0430\u043f\u0442\u0435\u0440\u043e\u0432 Bluetooth." }, "flow_title": "{name}", "step": { @@ -29,12 +29,18 @@ } } }, + "issues": { + "haos_outdated": { + "description": "\u0427\u0442\u043e\u0431\u044b \u043f\u043e\u0432\u044b\u0441\u0438\u0442\u044c \u043d\u0430\u0434\u0435\u0436\u043d\u043e\u0441\u0442\u044c \u0438 \u043f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0441\u0442\u044c Bluetooth, \u043c\u044b \u043d\u0430\u0441\u0442\u043e\u044f\u0442\u0435\u043b\u044c\u043d\u043e \u0440\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0435\u043c \u0412\u0430\u043c \u043e\u0431\u043d\u043e\u0432\u0438\u0442\u044c Home Assistant Operating System \u0434\u043e \u0432\u0435\u0440\u0441\u0438\u0438 9.0 \u0438\u043b\u0438 \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0437\u0434\u043d\u0435\u0439.", + "title": "\u041e\u0431\u043d\u043e\u0432\u0438\u0442\u0435 Home Assistant Operating System \u0434\u043e \u0432\u0435\u0440\u0441\u0438\u0438 9.0 \u0438\u043b\u0438 \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0437\u0434\u043d\u0435\u0439" + } + }, "options": { "step": { "init": { "data": { "adapter": "\u0410\u0434\u0430\u043f\u0442\u0435\u0440 Bluetooth, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0431\u0443\u0434\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0434\u043b\u044f \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f", - "passive": "\u041f\u0430\u0441\u0441\u0438\u0432\u043d\u043e\u0435 \u0441\u043b\u0443\u0448\u0430\u043d\u0438\u0435" + "passive": "\u041f\u0430\u0441\u0441\u0438\u0432\u043d\u043e\u0435 \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435" }, "description": "\u0414\u043b\u044f \u043f\u0430\u0441\u0441\u0438\u0432\u043d\u043e\u0433\u043e \u043f\u0440\u043e\u0441\u043b\u0443\u0448\u0438\u0432\u0430\u043d\u0438\u044f \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f BlueZ 5.63 \u0438\u043b\u0438 \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0437\u0434\u043d\u044f\u044f \u0432\u0435\u0440\u0441\u0438\u044f \u0441 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044b\u043c\u0438 \u044d\u043a\u0441\u043f\u0435\u0440\u0438\u043c\u0435\u043d\u0442\u0430\u043b\u044c\u043d\u044b\u043c\u0438 \u0444\u0443\u043d\u043a\u0446\u0438\u044f\u043c\u0438." } diff --git a/homeassistant/components/bluetooth/translations/sv.json b/homeassistant/components/bluetooth/translations/sv.json index fe07d338101..4f1e43c1490 100644 --- a/homeassistant/components/bluetooth/translations/sv.json +++ b/homeassistant/components/bluetooth/translations/sv.json @@ -12,6 +12,15 @@ "enable_bluetooth": { "description": "Vill du s\u00e4tta upp Bluetooth?" }, + "multiple_adapters": { + "data": { + "adapter": "Adapter" + }, + "description": "V\u00e4lj en Bluetooth-adapter som ska konfigureras" + }, + "single_adapter": { + "description": "Vill du konfigurera Bluetooth-adaptern {name} ?" + }, "user": { "data": { "address": "Enhet" @@ -24,8 +33,10 @@ "step": { "init": { "data": { - "adapter": "Bluetooth-adaptern som ska anv\u00e4ndas f\u00f6r skanning" - } + "adapter": "Bluetooth-adaptern som ska anv\u00e4ndas f\u00f6r skanning", + "passive": "Passiv skanning" + }, + "description": "Passiv lyssning kr\u00e4ver BlueZ 5.63 eller senare med experimentella funktioner aktiverade." } } } diff --git a/homeassistant/components/bluetooth/translations/tr.json b/homeassistant/components/bluetooth/translations/tr.json index e2286fcd122..2ffd8c80814 100644 --- a/homeassistant/components/bluetooth/translations/tr.json +++ b/homeassistant/components/bluetooth/translations/tr.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", - "no_adapters": "Bluetooth adapt\u00f6r\u00fc bulunamad\u0131" + "no_adapters": "Yap\u0131land\u0131r\u0131lmam\u0131\u015f Bluetooth adapt\u00f6r\u00fc bulunamad\u0131" }, "flow_title": "{name}", "step": { @@ -33,8 +33,10 @@ "step": { "init": { "data": { - "adapter": "Tarama i\u00e7in kullan\u0131lacak Bluetooth Adapt\u00f6r\u00fc" - } + "adapter": "Tarama i\u00e7in kullan\u0131lacak Bluetooth Adapt\u00f6r\u00fc", + "passive": "Pasif tarama" + }, + "description": "Pasif dinleme, BlueZ 5.63 veya daha yenisini ve deneysel \u00f6zelliklerin etkinle\u015ftirilmesini gerektirir." } } } diff --git a/homeassistant/components/bluetooth/translations/zh-Hant.json b/homeassistant/components/bluetooth/translations/zh-Hant.json index 08b19a67d34..a45ccc52d44 100644 --- a/homeassistant/components/bluetooth/translations/zh-Hant.json +++ b/homeassistant/components/bluetooth/translations/zh-Hant.json @@ -29,6 +29,12 @@ } } }, + "issues": { + "haos_outdated": { + "description": "\u6b32\u6539\u5584\u85cd\u82bd\u53ef\u9760\u6027\u8207\u6548\u80fd\uff0c\u5f37\u70c8\u5efa\u8b70\u60a8\u66f4\u65b0\u81f3 9.0 \u6216\u66f4\u65b0\u7248\u672c\u4e4b Home Assistant OS\u3002", + "title": "\u8acb\u66f4\u65b0\u81f3 Home Assistant OS 9.0 \u6216\u66f4\u65b0\u7248\u672c" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/bluetooth/update_coordinator.py b/homeassistant/components/bluetooth/update_coordinator.py index 9348095f2b1..2c99f189852 100644 --- a/homeassistant/components/bluetooth/update_coordinator.py +++ b/homeassistant/components/bluetooth/update_coordinator.py @@ -1,8 +1,8 @@ """Update coordinator for the Bluetooth integration.""" from __future__ import annotations +from abc import abstractmethod import logging -import time from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback @@ -11,6 +11,8 @@ from . import ( BluetoothChange, BluetoothScanningMode, BluetoothServiceInfoBleak, + async_address_present, + async_last_service_info, async_register_callback, async_track_unavailable, ) @@ -33,14 +35,13 @@ class BasePassiveBluetoothCoordinator: """Initialize the coordinator.""" self.hass = hass self.logger = logger - self.name: str | None = None self.address = address self.connectable = connectable self._cancel_track_unavailable: CALLBACK_TYPE | None = None self._cancel_bluetooth_advertisements: CALLBACK_TYPE | None = None - self._present = False self.mode = mode - self.last_seen = 0.0 + self._last_unavailable_time = 0.0 + self._last_name = address @callback def async_start(self) -> CALLBACK_TYPE: @@ -53,10 +54,41 @@ class BasePassiveBluetoothCoordinator: return _async_cancel + @callback + @abstractmethod + def _async_handle_bluetooth_event( + self, + service_info: BluetoothServiceInfoBleak, + change: BluetoothChange, + ) -> None: + """Handle a bluetooth event.""" + + @property + def name(self) -> str: + """Return last known name of the device.""" + if service_info := async_last_service_info( + self.hass, self.address, self.connectable + ): + return service_info.name + return self._last_name + + @property + def last_seen(self) -> float: + """Return the last time the device was seen.""" + # If the device is unavailable it will not have a service + # info and fall through below. + if service_info := async_last_service_info( + self.hass, self.address, self.connectable + ): + return service_info.time + # This is the time from the last advertisement that + # was set when the unavailable callback was called. + return self._last_unavailable_time + @property def available(self) -> bool: """Return if the device is available.""" - return self._present + return async_address_present(self.hass, self.address, self.connectable) @callback def _async_start(self) -> None: @@ -84,17 +116,9 @@ class BasePassiveBluetoothCoordinator: self._cancel_track_unavailable = None @callback - def _async_handle_unavailable(self, address: str) -> None: - """Handle the device going unavailable.""" - self._present = False - - @callback - def _async_handle_bluetooth_event( - self, - service_info: BluetoothServiceInfoBleak, - change: BluetoothChange, + def _async_handle_unavailable( + self, service_info: BluetoothServiceInfoBleak ) -> None: - """Handle a Bluetooth event.""" - self.last_seen = time.monotonic() - self.name = service_info.name - self._present = True + """Handle the device going unavailable.""" + self._last_unavailable_time = service_info.time + self._last_name = service_info.name diff --git a/homeassistant/components/bluetooth/usage.py b/homeassistant/components/bluetooth/usage.py index d282ca7415b..ba174f0306a 100644 --- a/homeassistant/components/bluetooth/usage.py +++ b/homeassistant/components/bluetooth/usage.py @@ -3,20 +3,39 @@ from __future__ import annotations import bleak +from bleak.backends.service import BleakGATTServiceCollection +import bleak_retry_connector from .models import HaBleakClientWrapper, HaBleakScannerWrapper ORIGINAL_BLEAK_SCANNER = bleak.BleakScanner ORIGINAL_BLEAK_CLIENT = bleak.BleakClient +ORIGINAL_BLEAK_RETRY_CONNECTOR_CLIENT = ( + bleak_retry_connector.BleakClientWithServiceCache +) def install_multiple_bleak_catcher() -> None: """Wrap the bleak classes to return the shared instance if 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] 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] + + +class HaBleakClientWithServiceCache(HaBleakClientWrapper): + """A BleakClient that implements service caching.""" + + def set_cached_services(self, services: BleakGATTServiceCollection | None) -> None: + """Set the cached services. + + No longer used since bleak 0.17+ has service caching built-in. + + This was only kept for backwards compatibility. + """ diff --git a/homeassistant/components/bluetooth/util.py b/homeassistant/components/bluetooth/util.py index 19efab7a15c..860428a6106 100644 --- a/homeassistant/components/bluetooth/util.py +++ b/homeassistant/components/bluetooth/util.py @@ -2,6 +2,7 @@ from __future__ import annotations import platform +import time from bluetooth_auto_recovery import recover_adapter @@ -15,6 +16,38 @@ from .const import ( WINDOWS_DEFAULT_BLUETOOTH_ADAPTER, AdapterDetails, ) +from .models import BluetoothServiceInfoBleak + + +async def async_load_history_from_system() -> dict[str, BluetoothServiceInfoBleak]: + """Load the device and advertisement_data history if available on the current system.""" + if platform.system() != "Linux": + return {} + from bluetooth_adapters import ( # pylint: disable=import-outside-toplevel + BlueZDBusObjects, + ) + + bluez_dbus = BlueZDBusObjects() + await bluez_dbus.load() + now = time.monotonic() + return { + address: BluetoothServiceInfoBleak( + name=history.advertisement_data.local_name + or history.device.name + or history.device.address, + address=history.device.address, + rssi=history.device.rssi, + manufacturer_data=history.advertisement_data.manufacturer_data, + service_data=history.advertisement_data.service_data, + service_uuids=history.advertisement_data.service_uuids, + source=history.source, + device=history.device, + advertisement=history.advertisement_data, + connectable=False, + time=now, + ) + for address, history in bluez_dbus.history.items() + } async def async_get_bluetooth_adapters() -> dict[str, AdapterDetails]: diff --git a/homeassistant/components/bluetooth_le_tracker/device_tracker.py b/homeassistant/components/bluetooth_le_tracker/device_tracker.py index 85908cc1d55..179d038e96e 100644 --- a/homeassistant/components/bluetooth_le_tracker/device_tracker.py +++ b/homeassistant/components/bluetooth_le_tracker/device_tracker.py @@ -12,10 +12,8 @@ import voluptuous as vol from homeassistant.components import bluetooth from homeassistant.components.bluetooth.match import BluetoothCallbackMatcher from homeassistant.components.device_tracker import ( - PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, -) -from homeassistant.components.device_tracker.const import ( CONF_TRACK_NEW, + PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, SCAN_INTERVAL, SourceType, ) diff --git a/homeassistant/components/bluetooth_tracker/device_tracker.py b/homeassistant/components/bluetooth_tracker/device_tracker.py index d266ba5d542..ce8f6ca8006 100644 --- a/homeassistant/components/bluetooth_tracker/device_tracker.py +++ b/homeassistant/components/bluetooth_tracker/device_tracker.py @@ -12,12 +12,10 @@ from bt_proximity import BluetoothRSSI import voluptuous as vol from homeassistant.components.device_tracker import ( - PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, -) -from homeassistant.components.device_tracker.const import ( CONF_SCAN_INTERVAL, CONF_TRACK_NEW, DEFAULT_TRACK_NEW, + PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, SCAN_INTERVAL, SourceType, ) diff --git a/homeassistant/components/bond/translations/bg.json b/homeassistant/components/bond/translations/bg.json index 7f67a133aa8..da903ee46b1 100644 --- a/homeassistant/components/bond/translations/bg.json +++ b/homeassistant/components/bond/translations/bg.json @@ -10,6 +10,9 @@ }, "flow_title": "{name} ({host})", "step": { + "confirm": { + "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 {name}?" + }, "user": { "data": { "host": "\u0425\u043e\u0441\u0442" diff --git a/homeassistant/components/bosch_shc/translations/cs.json b/homeassistant/components/bosch_shc/translations/cs.json index 45e02001105..ff70a713b06 100644 --- a/homeassistant/components/bosch_shc/translations/cs.json +++ b/homeassistant/components/bosch_shc/translations/cs.json @@ -1,12 +1,23 @@ { "config": { "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" }, "error": { "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "reauth_confirm": { + "title": "Znovu ov\u011b\u0159it integraci" + }, + "user": { + "data": { + "host": "Hostitel" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/bosch_shc/translations/es.json b/homeassistant/components/bosch_shc/translations/es.json index b2934bca747..bf4b20914ec 100644 --- a/homeassistant/components/bosch_shc/translations/es.json +++ b/homeassistant/components/bosch_shc/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "error": { "cannot_connect": "No se pudo conectar", diff --git a/homeassistant/components/bosch_shc/translations/pt.json b/homeassistant/components/bosch_shc/translations/pt.json index e229572938d..1462f7a14a0 100644 --- a/homeassistant/components/bosch_shc/translations/pt.json +++ b/homeassistant/components/bosch_shc/translations/pt.json @@ -1,7 +1,12 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida" + }, "error": { "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "unknown": "Erro inesperado" }, "step": { diff --git a/homeassistant/components/braviatv/__init__.py b/homeassistant/components/braviatv/__init__.py index 539dd980ffc..d06482e5c71 100644 --- a/homeassistant/components/braviatv/__init__.py +++ b/homeassistant/components/braviatv/__init__.py @@ -11,10 +11,14 @@ from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PIN, 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 CONF_IGNORED_SOURCES, CONF_USE_PSK, DOMAIN from .coordinator import BraviaTVCoordinator -PLATFORMS: Final[list[Platform]] = [Platform.MEDIA_PLAYER, Platform.REMOTE] +PLATFORMS: Final[list[Platform]] = [ + Platform.BUTTON, + Platform.MEDIA_PLAYER, + Platform.REMOTE, +] async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: @@ -22,13 +26,20 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b host = config_entry.data[CONF_HOST] mac = config_entry.data[CONF_MAC] pin = config_entry.data[CONF_PIN] + use_psk = config_entry.data.get(CONF_USE_PSK, False) 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) - coordinator = BraviaTVCoordinator(hass, client, pin, ignored_sources) + coordinator = BraviaTVCoordinator( + hass=hass, + client=client, + pin=pin, + use_psk=use_psk, + ignored_sources=ignored_sources, + ) config_entry.async_on_unload(config_entry.add_update_listener(update_listener)) await coordinator.async_config_entry_first_refresh() diff --git a/homeassistant/components/braviatv/button.py b/homeassistant/components/braviatv/button.py new file mode 100644 index 00000000000..6cc0cb393c5 --- /dev/null +++ b/homeassistant/components/braviatv/button.py @@ -0,0 +1,89 @@ +"""Button support for Bravia TV.""" +from __future__ import annotations + +from collections.abc import Callable, Coroutine +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 BraviaTVCoordinator +from .entity import BraviaTVEntity + + +@dataclass +class BraviaTVButtonDescriptionMixin: + """Mixin to describe a Bravia TV Button entity.""" + + press_action: Callable[[BraviaTVCoordinator], Coroutine] + + +@dataclass +class BraviaTVButtonDescription( + ButtonEntityDescription, BraviaTVButtonDescriptionMixin +): + """Bravia TV Button description.""" + + +BUTTONS: tuple[BraviaTVButtonDescription, ...] = ( + BraviaTVButtonDescription( + key="reboot", + name="Reboot", + device_class=ButtonDeviceClass.RESTART, + entity_category=EntityCategory.CONFIG, + press_action=lambda coordinator: coordinator.async_reboot_device(), + ), + BraviaTVButtonDescription( + key="terminate_apps", + name="Terminate apps", + entity_category=EntityCategory.CONFIG, + press_action=lambda coordinator: coordinator.async_terminate_apps(), + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Bravia TV Button entities.""" + + coordinator = hass.data[DOMAIN][config_entry.entry_id] + unique_id = config_entry.unique_id + assert unique_id is not None + + async_add_entities( + BraviaTVButton(coordinator, unique_id, config_entry.title, description) + for description in BUTTONS + ) + + +class BraviaTVButton(BraviaTVEntity, ButtonEntity): + """Representation of a Bravia TV Button.""" + + entity_description: BraviaTVButtonDescription + + def __init__( + self, + coordinator: BraviaTVCoordinator, + unique_id: str, + model: str, + description: BraviaTVButtonDescription, + ) -> None: + """Initialize the button.""" + super().__init__(coordinator, unique_id, model) + self._attr_unique_id = f"{unique_id}_{description.key}" + self.entity_description = description + + async def async_press(self) -> None: + """Trigger the button action.""" + await self.entity_description.press_action(self.coordinator) diff --git a/homeassistant/components/braviatv/config_flow.py b/homeassistant/components/braviatv/config_flow.py index f89880caf89..e6bf5a44019 100644 --- a/homeassistant/components/braviatv/config_flow.py +++ b/homeassistant/components/braviatv/config_flow.py @@ -1,22 +1,22 @@ """Config flow to configure the Bravia TV integration.""" from __future__ import annotations -from contextlib import suppress -import ipaddress -import re from typing import Any +from urllib.parse import urlparse from aiohttp import CookieJar -from pybravia import BraviaTV, BraviaTVError, BraviaTVNotSupported +from pybravia import BraviaTV, BraviaTVAuthError, BraviaTVError, BraviaTVNotSupported 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_PIN +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.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 ( @@ -25,29 +25,20 @@ from .const import ( ATTR_MODEL, CLIENTID_PREFIX, CONF_IGNORED_SOURCES, + CONF_USE_PSK, DOMAIN, NICKNAME, ) -def host_valid(host: str) -> bool: - """Return True if hostname or IP address is valid.""" - with suppress(ValueError): - if ipaddress.ip_address(host).version in [4, 6]: - return True - disallowed = re.compile(r"[^a-zA-Z\d\-]") - return all(x and not disallowed.search(x) for x in host.split(".")) - - class BraviaTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Bravia TV integration.""" VERSION = 1 - client: BraviaTV - def __init__(self) -> None: """Initialize config flow.""" + self.client: BraviaTV | None = None self.device_config: dict[str, Any] = {} @staticmethod @@ -56,11 +47,28 @@ class BraviaTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Bravia TV options callback.""" return BraviaTVOptionsFlowHandler(config_entry) - async def async_init_device(self) -> FlowResult: - """Initialize and create Bravia TV device from config.""" - pin = self.device_config[CONF_PIN] + def create_client(self) -> None: + """Create Bravia TV client from config.""" + host = self.device_config[CONF_HOST] + session = async_create_clientsession( + self.hass, + cookie_jar=CookieJar(unsafe=True, quote_cookie=False), + ) + self.client = BraviaTV(host=host, session=session) - await self.client.connect(pin=pin, clientid=CLIENTID_PREFIX, nickname=NICKNAME) + async def async_create_device(self) -> FlowResult: + """Initialize and create Bravia TV device from config.""" + assert self.client + + pin = self.device_config[CONF_PIN] + use_psk = self.device_config[CONF_USE_PSK] + + if use_psk: + await self.client.connect(psk=pin) + else: + await self.client.connect( + pin=pin, clientid=CLIENTID_PREFIX, nickname=NICKNAME + ) await self.client.set_wol_mode(True) system_info = await self.client.get_system_info() @@ -82,14 +90,9 @@ class BraviaTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if user_input is not None: host = user_input[CONF_HOST] - if host_valid(host): - session = async_create_clientsession( - self.hass, - cookie_jar=CookieJar(unsafe=True, quote_cookie=False), - ) - self.client = BraviaTV(host=host, session=session) + if is_host_valid(host): self.device_config[CONF_HOST] = host - + self.create_client() return await self.async_step_authorize() errors[CONF_HOST] = "invalid_host" @@ -103,18 +106,23 @@ class BraviaTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_authorize( self, user_input: dict[str, Any] | None = None ) -> FlowResult: - """Get PIN from the Bravia TV device.""" + """Authorize Bravia TV device.""" errors: dict[str, str] = {} if user_input is not None: self.device_config[CONF_PIN] = user_input[CONF_PIN] + self.device_config[CONF_USE_PSK] = user_input[CONF_USE_PSK] try: - return await self.async_init_device() + return await self.async_create_device() + except BraviaTVAuthError: + errors["base"] = "invalid_auth" except BraviaTVNotSupported: errors["base"] = "unsupported_model" except BraviaTVError: errors["base"] = "cannot_connect" + assert self.client + try: await self.client.pair(CLIENTID_PREFIX, NICKNAME) except BraviaTVError: @@ -122,10 +130,53 @@ class BraviaTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_show_form( step_id="authorize", - data_schema=vol.Schema({vol.Required(CONF_PIN, default=""): str}), + data_schema=vol.Schema( + { + vol.Required(CONF_PIN, default=""): str, + vol.Required(CONF_USE_PSK, default=False): bool, + } + ), errors=errors, ) + async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult: + """Handle a discovered device.""" + parsed_url = urlparse(discovery_info.ssdp_location) + host = parsed_url.hostname + + await self.async_set_unique_id(discovery_info.upnp[ssdp.ATTR_UPNP_UDN]) + self._abort_if_unique_id_configured(updates={CONF_HOST: host}) + self._async_abort_entries_match({CONF_HOST: host}) + + scalarweb_info = discovery_info.upnp["X_ScalarWebAPI_DeviceInfo"] + service_types = scalarweb_info["X_ScalarWebAPI_ServiceList"][ + "X_ScalarWebAPI_ServiceType" + ] + + if "videoScreen" not in service_types: + return self.async_abort(reason="not_bravia_device") + + model_name = discovery_info.upnp[ssdp.ATTR_UPNP_MODEL_NAME] + friendly_name = discovery_info.upnp[ssdp.ATTR_UPNP_FRIENDLY_NAME] + + self.context["title_placeholders"] = { + CONF_NAME: f"{model_name} ({friendly_name})", + CONF_HOST: host, + } + + self.device_config[CONF_HOST] = host + return await self.async_step_confirm() + + async def async_step_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Allow the user to confirm adding the device.""" + if user_input is not None: + self.create_client() + return await self.async_step_authorize() + + return self.async_show_form(step_id="confirm") + class BraviaTVOptionsFlowHandler(config_entries.OptionsFlow): """Config flow options for Bravia TV.""" diff --git a/homeassistant/components/braviatv/const.py b/homeassistant/components/braviatv/const.py index 6ed8efd3739..8855499914c 100644 --- a/homeassistant/components/braviatv/const.py +++ b/homeassistant/components/braviatv/const.py @@ -9,6 +9,7 @@ ATTR_MANUFACTURER: Final = "Sony" ATTR_MODEL: Final = "model" CONF_IGNORED_SOURCES: Final = "ignored_sources" +CONF_USE_PSK: Final = "use_psk" CLIENTID_PREFIX: Final = "HomeAssistant" DOMAIN: Final = "braviatv" diff --git a/homeassistant/components/braviatv/coordinator.py b/homeassistant/components/braviatv/coordinator.py index 49c902e0d44..f8128483852 100644 --- a/homeassistant/components/braviatv/coordinator.py +++ b/homeassistant/components/braviatv/coordinator.py @@ -13,13 +13,11 @@ from pybravia import ( BraviaTVConnectionTimeout, BraviaTVError, BraviaTVNotFound, + BraviaTVTurnedOff, ) from typing_extensions import Concatenate, ParamSpec -from homeassistant.components.media_player.const import ( - MEDIA_TYPE_APP, - MEDIA_TYPE_CHANNEL, -) +from homeassistant.components.media_player import MediaType from homeassistant.core import HomeAssistant from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -62,19 +60,21 @@ class BraviaTVCoordinator(DataUpdateCoordinator[None]): hass: HomeAssistant, client: BraviaTV, pin: str, + use_psk: bool, ignored_sources: list[str], ) -> None: """Initialize Bravia TV Client.""" self.client = client self.pin = pin + self.use_psk = use_psk 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_content_id: str | None = None - self.media_content_type: str | None = None + self.media_content_type: MediaType | None = None self.media_uri: str | None = None self.media_duration: int | None = None self.volume_level: float | None = None @@ -113,9 +113,12 @@ class BraviaTVCoordinator(DataUpdateCoordinator[None]): """Connect and fetch data.""" try: if not self.connected: - await self.client.connect( - pin=self.pin, clientid=CLIENTID_PREFIX, nickname=NICKNAME - ) + if self.use_psk: + await self.client.connect(psk=self.pin) + else: + await self.client.connect( + pin=self.pin, clientid=CLIENTID_PREFIX, nickname=NICKNAME + ) self.connected = True power_status = await self.client.get_power_status() @@ -136,7 +139,7 @@ class BraviaTVCoordinator(DataUpdateCoordinator[None]): _LOGGER.debug("Update skipped, Bravia API service is reloading") return raise UpdateFailed("Error communicating with device") from err - except (BraviaTVConnectionError, BraviaTVConnectionTimeout): + except (BraviaTVConnectionError, BraviaTVConnectionTimeout, BraviaTVTurnedOff): self.is_on = False self.connected = False _LOGGER.debug("Update skipped, Bravia TV is off") @@ -182,7 +185,7 @@ class BraviaTVCoordinator(DataUpdateCoordinator[None]): self.is_channel = self.media_uri[:2] == "tv" if self.is_channel: self.media_content_id = playing_info.get("dispNum") - self.media_content_type = MEDIA_TYPE_CHANNEL + self.media_content_type = MediaType.CHANNEL else: self.media_content_id = self.media_uri self.media_content_type = None @@ -193,7 +196,7 @@ class BraviaTVCoordinator(DataUpdateCoordinator[None]): self.media_content_type = None if not playing_info: self.media_title = "Smart TV" - self.media_content_type = MEDIA_TYPE_APP + self.media_content_type = MediaType.APP @catch_braviatv_errors async def async_turn_on(self) -> None: @@ -285,3 +288,13 @@ class BraviaTVCoordinator(DataUpdateCoordinator[None]): cmd, commands_keys, ) + + @catch_braviatv_errors + async def async_reboot_device(self) -> None: + """Send command to reboot the device.""" + await self.client.reboot() + + @catch_braviatv_errors + async def async_terminate_apps(self) -> None: + """Send command to terminate all applications.""" + await self.client.terminate_apps() diff --git a/homeassistant/components/braviatv/manifest.json b/homeassistant/components/braviatv/manifest.json index dca9d65cff0..ffb92a2348f 100644 --- a/homeassistant/components/braviatv/manifest.json +++ b/homeassistant/components/braviatv/manifest.json @@ -2,8 +2,14 @@ "domain": "braviatv", "name": "Sony Bravia TV", "documentation": "https://www.home-assistant.io/integrations/braviatv", - "requirements": ["pybravia==0.2.2"], + "requirements": ["pybravia==0.2.3"], "codeowners": ["@bieniu", "@Drafteed"], + "ssdp": [ + { + "st": "urn:schemas-sony-com:service:ScalarWebAPI:1", + "manufacturer": "Sony Corporation" + } + ], "config_flow": true, "iot_class": "local_polling", "loggers": ["pybravia"] diff --git a/homeassistant/components/braviatv/media_player.py b/homeassistant/components/braviatv/media_player.py index 525e265d415..65a8e46946e 100644 --- a/homeassistant/components/braviatv/media_player.py +++ b/homeassistant/components/braviatv/media_player.py @@ -5,9 +5,10 @@ from homeassistant.components.media_player import ( MediaPlayerDeviceClass, MediaPlayerEntity, MediaPlayerEntityFeature, + MediaPlayerState, + MediaType, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import STATE_OFF, STATE_PAUSED, STATE_PLAYING from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -50,11 +51,15 @@ class BraviaTVMediaPlayer(BraviaTVEntity, MediaPlayerEntity): ) @property - def state(self) -> str | None: + def state(self) -> MediaPlayerState: """Return the state of the device.""" if self.coordinator.is_on: - return STATE_PLAYING if self.coordinator.playing else STATE_PAUSED - return STATE_OFF + return ( + MediaPlayerState.PLAYING + if self.coordinator.playing + else MediaPlayerState.PAUSED + ) + return MediaPlayerState.OFF @property def source(self) -> str | None: @@ -87,7 +92,7 @@ class BraviaTVMediaPlayer(BraviaTVEntity, MediaPlayerEntity): return self.coordinator.media_content_id @property - def media_content_type(self) -> str | None: + def media_content_type(self) -> MediaType | None: """Content type of current playing media.""" return self.coordinator.media_content_type diff --git a/homeassistant/components/braviatv/strings.json b/homeassistant/components/braviatv/strings.json index c00b143a442..f6c35f2b8ca 100644 --- a/homeassistant/components/braviatv/strings.json +++ b/homeassistant/components/braviatv/strings.json @@ -9,20 +9,26 @@ }, "authorize": { "title": "Authorize Sony Bravia TV", - "description": "Enter the PIN code shown on the Sony Bravia TV. \n\nIf the PIN code is not shown, you have to unregister Home Assistant on your TV, go to: Settings -> Network -> Remote device settings -> Unregister remote device.", + "description": "Enter the PIN code shown on the Sony Bravia TV. \n\nIf the PIN code is not shown, you have to unregister Home Assistant on your TV, go to: Settings -> Network -> Remote device settings -> Deregister remote device. \n\nYou can use PSK (Pre-Shared-Key) instead of PIN. PSK is a user-defined secret key used for access control. This authentication method is recommended as more stable. To enable PSK on your TV, go to: Settings -> Network -> Home Network Setup -> IP Control. Then check «Use PSK authentication» box and enter your PSK instead of PIN.", "data": { - "pin": "[%key:common::config_flow::data::pin%]" + "pin": "[%key:common::config_flow::data::pin%]", + "use_psk": "Use PSK authentication" } + }, + "confirm": { + "description": "[%key:common::config_flow::description::confirm_setup%]" } }, "error": { "invalid_host": "[%key:common::config_flow::error::invalid_host%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "unsupported_model": "Your TV model is not supported." }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "no_ip_control": "IP Control is disabled on your TV or the TV is not supported." + "no_ip_control": "IP Control is disabled on your TV or the TV is not supported.", + "not_bravia_device": "The device is not a Bravia TV." } }, "options": { diff --git a/homeassistant/components/braviatv/translations/bg.json b/homeassistant/components/braviatv/translations/bg.json index ef8edbdab20..f43846e2283 100644 --- a/homeassistant/components/braviatv/translations/bg.json +++ b/homeassistant/components/braviatv/translations/bg.json @@ -1,10 +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" + "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." }, "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", "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", "unsupported_model": "\u041c\u043e\u0434\u0435\u043b\u044a\u0442 \u043d\u0430 \u0432\u0430\u0448\u0438\u044f \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440 \u043d\u0435 \u0441\u0435 \u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430." }, @@ -14,6 +16,9 @@ "pin": "\u041f\u0418\u041d \u043a\u043e\u0434" } }, + "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?" + }, "user": { "data": { "host": "\u0425\u043e\u0441\u0442" diff --git a/homeassistant/components/braviatv/translations/ca.json b/homeassistant/components/braviatv/translations/ca.json index 2f35c77caa1..6c8c9736750 100644 --- a/homeassistant/components/braviatv/translations/ca.json +++ b/homeassistant/components/braviatv/translations/ca.json @@ -2,21 +2,27 @@ "config": { "abort": { "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." + "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." }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", "invalid_host": "Nom de l'amfitri\u00f3 o adre\u00e7a IP inv\u00e0lids", "unsupported_model": "Aquest model de televisor no \u00e9s compatible." }, "step": { "authorize": { "data": { - "pin": "Codi PIN" + "pin": "Codi PIN", + "use_psk": "Utilitza autenticaci\u00f3 PSK" }, "description": "Introdueix el codi PIN que es mostra a la pantalla del televisor.\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.", "title": "Autoritzaci\u00f3 del televisor Sony Bravia" }, + "confirm": { + "description": "Vols comen\u00e7ar la configuraci\u00f3?" + }, "user": { "data": { "host": "Amfitri\u00f3" diff --git a/homeassistant/components/braviatv/translations/de.json b/homeassistant/components/braviatv/translations/de.json index 035de7bc060..18863481bc0 100644 --- a/homeassistant/components/braviatv/translations/de.json +++ b/homeassistant/components/braviatv/translations/de.json @@ -2,21 +2,27 @@ "config": { "abort": { "already_configured": "Ger\u00e4t ist bereits konfiguriert", - "no_ip_control": "IP-Steuerung ist auf deinen Fernseher deaktiviert oder der Fernseher wird nicht unterst\u00fctzt." + "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." }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", "invalid_host": "Ung\u00fcltiger Hostname oder IP-Adresse", "unsupported_model": "Dein TV-Modell wird nicht unterst\u00fctzt." }, "step": { "authorize": { "data": { - "pin": "PIN-Code" + "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 daf\u00fcr zu: Einstellungen \u2192 Netzwerk \u2192 Remote - Ger\u00e4teeinstellungen \u2192 Registrierung des entfernten Ger\u00e4ts aufheben.", + "description": "Gib den auf dem Sony Bravia-Fernseher angezeigten PIN-Code ein. \n\nWenn der PIN-Code nicht angezeigt wird, musst du die Registrierung von Home Assistant auf Ihrem Fernseher aufheben, gehe zu: Einstellungen - > Netzwerk - > Remote-Ger\u00e4teeinstellungen - > Remote-Ger\u00e4t abmelden. \n\nDu kannst PSK (Pre-Shared-Key) anstelle der PIN verwenden. PSK ist ein benutzerdefinierter geheimer Schl\u00fcssel, der f\u00fcr die Zugriffskontrolle verwendet wird. Diese Authentifizierungsmethode wird als stabiler empfohlen. Um PSK auf deinem Fernseher zu aktivieren, gehe zu: Einstellungen - > Netzwerk - > Heimnetzwerk-Setup - > IP-Steuerung. Aktiviere dann das Kontrollk\u00e4stchen \u00abPSK-Authentifizierung verwenden\u00bb und gib deinen PSK anstelle der PIN ein.", "title": "Autorisiere Sony Bravia TV" }, + "confirm": { + "description": "M\u00f6chtest Du mit der Einrichtung beginnen?" + }, "user": { "data": { "host": "Host" diff --git a/homeassistant/components/braviatv/translations/en.json b/homeassistant/components/braviatv/translations/en.json index 45722d13b9a..7401fda7324 100644 --- a/homeassistant/components/braviatv/translations/en.json +++ b/homeassistant/components/braviatv/translations/en.json @@ -2,21 +2,27 @@ "config": { "abort": { "already_configured": "Device is already configured", - "no_ip_control": "IP Control is disabled on your TV or the TV is not supported." + "no_ip_control": "IP Control is disabled on your TV or the TV is not supported.", + "not_bravia_device": "The device is not a Bravia TV." }, "error": { "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", "invalid_host": "Invalid hostname or IP address", "unsupported_model": "Your TV model is not supported." }, "step": { "authorize": { "data": { - "pin": "PIN Code" + "pin": "PIN Code", + "use_psk": "Use PSK authentication" }, - "description": "Enter the PIN code shown on the Sony Bravia TV. \n\nIf the PIN code is not shown, you have to unregister Home Assistant on your TV, go to: Settings -> Network -> Remote device settings -> Unregister remote device.", + "description": "Enter the PIN code shown on the Sony Bravia TV. \n\nIf the PIN code is not shown, you have to unregister Home Assistant on your TV, go to: Settings -> Network -> Remote device settings -> Deregister remote device. \n\nYou can use PSK (Pre-Shared-Key) instead of PIN. PSK is a user-defined secret key used for access control. This authentication method is recommended as more stable. To enable PSK on your TV, go to: Settings -> Network -> Home Network Setup -> IP Control. Then check \u00abUse PSK authentication\u00bb box and enter your PSK instead of PIN.", "title": "Authorize Sony Bravia TV" }, + "confirm": { + "description": "Do you want to start set up?" + }, "user": { "data": { "host": "Host" diff --git a/homeassistant/components/braviatv/translations/es.json b/homeassistant/components/braviatv/translations/es.json index 86436249717..325ccb4c535 100644 --- a/homeassistant/components/braviatv/translations/es.json +++ b/homeassistant/components/braviatv/translations/es.json @@ -2,21 +2,27 @@ "config": { "abort": { "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." + "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." }, "error": { "cannot_connect": "No se pudo conectar", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "invalid_host": "Nombre de host o direcci\u00f3n IP no v\u00e1lidos", "unsupported_model": "Tu modelo de TV no es compatible." }, "step": { "authorize": { "data": { - "pin": "C\u00f3digo PIN" + "pin": "C\u00f3digo PIN", + "use_psk": "Usar autenticaci\u00f3n PSK" }, - "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.", + "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.", "title": "Autorizar Sony Bravia TV" }, + "confirm": { + "description": "\u00bfQuieres iniciar la configuraci\u00f3n?" + }, "user": { "data": { "host": "Host" diff --git a/homeassistant/components/braviatv/translations/fr.json b/homeassistant/components/braviatv/translations/fr.json index 0290dad6857..73a32d0a06a 100644 --- a/homeassistant/components/braviatv/translations/fr.json +++ b/homeassistant/components/braviatv/translations/fr.json @@ -2,21 +2,27 @@ "config": { "abort": { "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." + "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." }, "error": { "cannot_connect": "\u00c9chec de connexion", + "invalid_auth": "Authentification non valide", "invalid_host": "Nom d'h\u00f4te ou adresse IP non valide", "unsupported_model": "Votre mod\u00e8le de t\u00e9l\u00e9viseur n'est pas pris en charge." }, "step": { "authorize": { "data": { - "pin": "Code PIN" + "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.", "title": "Autoriser Sony Bravia TV" }, + "confirm": { + "description": "Voulez-vous commencer la configuration\u00a0?" + }, "user": { "data": { "host": "H\u00f4te" diff --git a/homeassistant/components/braviatv/translations/he.json b/homeassistant/components/braviatv/translations/he.json index ab9d638a8ac..b717638aa2f 100644 --- a/homeassistant/components/braviatv/translations/he.json +++ b/homeassistant/components/braviatv/translations/he.json @@ -5,6 +5,7 @@ }, "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", "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": { @@ -13,6 +14,9 @@ "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?" + }, "user": { "data": { "host": "\u05de\u05d0\u05e8\u05d7" diff --git a/homeassistant/components/braviatv/translations/hu.json b/homeassistant/components/braviatv/translations/hu.json index d0d372df898..02b64050ee2 100644 --- a/homeassistant/components/braviatv/translations/hu.json +++ b/homeassistant/components/braviatv/translations/hu.json @@ -2,21 +2,27 @@ "config": { "abort": { "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." + "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." }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", "invalid_host": "\u00c9rv\u00e9nytelen hosztn\u00e9v vagy IP-c\u00edm", "unsupported_model": "A TV modell nem t\u00e1mogatott." }, "step": { "authorize": { "data": { - "pin": "PIN-k\u00f3d" + "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, l\u00e9pjen a k\u00f6vetkez\u0151re: 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.", + "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" }, + "confirm": { + "description": "El szeretn\u00e9 kezdeni a be\u00e1ll\u00edt\u00e1st?" + }, "user": { "data": { "host": "C\u00edm" diff --git a/homeassistant/components/braviatv/translations/id.json b/homeassistant/components/braviatv/translations/id.json index e387a2113b0..63b4353aefc 100644 --- a/homeassistant/components/braviatv/translations/id.json +++ b/homeassistant/components/braviatv/translations/id.json @@ -2,21 +2,27 @@ "config": { "abort": { "already_configured": "Perangkat sudah dikonfigurasi", - "no_ip_control": "Kontrol IP dinonaktifkan di TV Anda atau TV tidak didukung." + "no_ip_control": "Kontrol IP dinonaktifkan di TV Anda atau TV tidak didukung.", + "not_bravia_device": "Perangkat ini bukan TV Bravia." }, "error": { "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid", "invalid_host": "Nama host atau alamat IP tidak valid", "unsupported_model": "Model TV Anda tidak didukung." }, "step": { "authorize": { "data": { - "pin": "Kode PIN" + "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.", + "description": "Masukkan kode PIN yang ditampilkan di TV Sony Bravia. \n\nJika kode PIN tidak ditampilkan, Anda harus membatalkan pendaftaran Home Assistant di TV, buka: Pengaturan -> Jaringan -> Pengaturan perangkat jarak jauh -> Batalkan pendaftaran perangkat jarak jauh.\n\nAnda bisa menggunakan PSK (Pre-Shared-Key) alih-alih menggunakan PIN. PSK merupakan kunci rahasia yang ditentukan pengguna untuk mengakses kontrol. Metode autentikasi ini disarankan karena lebih stabil. Untuk mengaktifkan PSK di TV Anda, buka Pengaturan -> Jaringan -> Penyiapan Jaringan Rumah -> Kontrol IP, lalu centang \u00abGunakan autentikasi PSK\u00bb dan masukkan PSK Anda, bukan PIN.", "title": "Otorisasi TV Sony Bravia" }, + "confirm": { + "description": "Ingin memulai penyiapan?" + }, "user": { "data": { "host": "Host" diff --git a/homeassistant/components/braviatv/translations/it.json b/homeassistant/components/braviatv/translations/it.json index 3dd57d1359a..8ac0b2d7df9 100644 --- a/homeassistant/components/braviatv/translations/it.json +++ b/homeassistant/components/braviatv/translations/it.json @@ -2,21 +2,27 @@ "config": { "abort": { "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", - "no_ip_control": "Il controllo IP \u00e8 disabilitato sulla TV o la TV non \u00e8 supportata." + "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." }, "error": { "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida", "invalid_host": "Nome host o indirizzo IP non valido", "unsupported_model": "Il tuo modello TV non \u00e8 supportato." }, "step": { "authorize": { "data": { - "pin": "Codice PIN" + "pin": "Codice PIN", + "use_psk": "Usa l'autenticazione PSK" }, - "description": "Immetti il codice PIN visualizzato sul Sony Bravia TV. \n\nSe il codice PIN non viene visualizzato, devi annullare la registrazione di Home Assistant sul televisore, vai su: Impostazioni - > Rete - > Impostazioni dispositivo remoto - > Annulla registrazione dispositivo remoto.", + "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.", "title": "Autorizza Sony Bravia TV" }, + "confirm": { + "description": "Vuoi iniziare la configurazione?" + }, "user": { "data": { "host": "Host" diff --git a/homeassistant/components/braviatv/translations/nl.json b/homeassistant/components/braviatv/translations/nl.json index d3696024643..18a4f8881fb 100644 --- a/homeassistant/components/braviatv/translations/nl.json +++ b/homeassistant/components/braviatv/translations/nl.json @@ -2,21 +2,27 @@ "config": { "abort": { "already_configured": "Apparaat is al geconfigureerd", - "no_ip_control": "IP-besturing is uitgeschakeld op uw tv of de tv wordt niet ondersteund." + "no_ip_control": "IP-besturing is uitgeschakeld op uw tv of de tv wordt niet ondersteund.", + "not_bravia_device": "Dit apparaat is geen Bravia-TV." }, "error": { "cannot_connect": "Kan geen verbinding maken", + "invalid_auth": "Ongeldige authenticatie", "invalid_host": "Ongeldige hostnaam of IP-adres", "unsupported_model": "Uw tv-model wordt niet ondersteund." }, "step": { "authorize": { "data": { - "pin": "Pincode" + "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.", "title": "Autoriseer Sony Bravia tv" }, + "confirm": { + "description": "Wil je beginnen met instellen?" + }, "user": { "data": { "host": "Host" diff --git a/homeassistant/components/braviatv/translations/no.json b/homeassistant/components/braviatv/translations/no.json index 067a2fcda97..7fa16b90e8a 100644 --- a/homeassistant/components/braviatv/translations/no.json +++ b/homeassistant/components/braviatv/translations/no.json @@ -2,21 +2,27 @@ "config": { "abort": { "already_configured": "Enheten er allerede konfigurert", - "no_ip_control": "IP-kontrollen er deaktivert p\u00e5 TVen eller TV-en st\u00f8ttes ikke." + "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." }, "error": { "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning", "invalid_host": "Ugyldig vertsnavn eller IP-adresse", "unsupported_model": "TV-modellen din st\u00f8ttes ikke." }, "step": { "authorize": { "data": { - "pin": "PIN kode" + "pin": "PIN kode", + "use_psk": "Bruk PSK-autentisering" }, - "description": "Angi PIN-koden som vises p\u00e5 Sony Bravia TV. \n\nHvis PIN-koden ikke vises, m\u00e5 du avregistrere Home Assistant p\u00e5 TV-en, g\u00e5 til: Innstillinger -> Nettverk -> Innstillinger for ekstern enhet -> Avregistrere ekstern enhet.", + "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.", "title": "Godkjenn Sony Bravia TV" }, + "confirm": { + "description": "Vil du starte oppsettet?" + }, "user": { "data": { "host": "Vert" diff --git a/homeassistant/components/braviatv/translations/pt-BR.json b/homeassistant/components/braviatv/translations/pt-BR.json index 3931935ff38..a6aef38d17f 100644 --- a/homeassistant/components/braviatv/translations/pt-BR.json +++ b/homeassistant/components/braviatv/translations/pt-BR.json @@ -2,21 +2,27 @@ "config": { "abort": { "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", - "no_ip_control": "O Controle de IP est\u00e1 desativado em sua TV ou a TV n\u00e3o \u00e9 compat\u00edvel." + "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." }, "error": { "cannot_connect": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "invalid_host": "Nome de host ou endere\u00e7o IP inv\u00e1lido", "unsupported_model": "Seu modelo de TV n\u00e3o \u00e9 suportado." }, "step": { "authorize": { "data": { - "pin": "C\u00f3digo PIN" + "pin": "C\u00f3digo PIN", + "use_psk": "Usar autentica\u00e7\u00e3o PSK" }, - "description": "Digite o c\u00f3digo PIN mostrado na TV Sony Bravia.\n\nSe o c\u00f3digo PIN n\u00e3o for mostrado, voc\u00ea deve cancelar o registro do Home Assistant na sua TV, v\u00e1 para: Configura\u00e7\u00f5es -> Rede -> Configura\u00e7\u00f5es do dispositivo remoto -> Cancelar registro do dispositivo remoto.", + "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.", "title": "Autorizar a TV Sony Bravia" }, + "confirm": { + "description": "Deseja iniciar a configura\u00e7\u00e3o?" + }, "user": { "data": { "host": "Nome do host" diff --git a/homeassistant/components/braviatv/translations/pt.json b/homeassistant/components/braviatv/translations/pt.json index 0838bb5d632..5ee36fdbb85 100644 --- a/homeassistant/components/braviatv/translations/pt.json +++ b/homeassistant/components/braviatv/translations/pt.json @@ -1,10 +1,12 @@ { "config": { "abort": { - "already_configured": "Esta TV j\u00e1 est\u00e1 configurada." + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "not_bravia_device": "O dispositivo n\u00e3o \u00e9 uma TV Bravia." }, "error": { "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "invalid_host": "Nome de servidor ou endere\u00e7o IP inv\u00e1lido.", "unsupported_model": "O seu modelo de TV n\u00e3o \u00e9 suportado." }, diff --git a/homeassistant/components/braviatv/translations/ru.json b/homeassistant/components/braviatv/translations/ru.json index 046a46c5ae4..f191e3607cc 100644 --- a/homeassistant/components/braviatv/translations/ru.json +++ b/homeassistant/components/braviatv/translations/ru.json @@ -2,21 +2,27 @@ "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_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." + "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." }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "invalid_host": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0434\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441.", "unsupported_model": "\u042d\u0442\u0430 \u043c\u043e\u0434\u0435\u043b\u044c \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0430 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f." }, "step": { "authorize": { "data": { - "pin": "PIN-\u043a\u043e\u0434" + "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.", + "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.", "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?" + }, "user": { "data": { "host": "\u0425\u043e\u0441\u0442" diff --git a/homeassistant/components/braviatv/translations/zh-Hant.json b/homeassistant/components/braviatv/translations/zh-Hant.json index 1fb0931d4b6..9753715eec1 100644 --- a/homeassistant/components/braviatv/translations/zh-Hant.json +++ b/homeassistant/components/braviatv/translations/zh-Hant.json @@ -2,21 +2,27 @@ "config": { "abort": { "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" + "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" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", "invalid_host": "\u7121\u6548\u4e3b\u6a5f\u540d\u7a31\u6216 IP \u4f4d\u5740", "unsupported_model": "\u4e0d\u652f\u63f4\u6b64\u6b3e\u96fb\u8996\u578b\u865f\u3002" }, "step": { "authorize": { "data": { - "pin": "PIN \u78bc" + "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", + "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", "title": "\u8a8d\u8b49 Sony Bravia \u96fb\u8996" }, + "confirm": { + "description": "\u662f\u5426\u8981\u958b\u59cb\u8a2d\u5b9a\uff1f" + }, "user": { "data": { "host": "\u4e3b\u6a5f\u7aef" diff --git a/homeassistant/components/brother/__init__.py b/homeassistant/components/brother/__init__.py index 7c3d3003ea6..c1dbfd5bf0a 100644 --- a/homeassistant/components/brother/__init__.py +++ b/homeassistant/components/brother/__init__.py @@ -5,12 +5,12 @@ from datetime import timedelta import logging import async_timeout -from brother import Brother, DictToObj, SnmpError, UnsupportedModel -import pysnmp.hlapi.asyncio as SnmpEngine +from brother import Brother, BrotherSensors, SnmpError, UnsupportedModel from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_TYPE, Platform from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import DATA_CONFIG_ENTRY, DOMAIN, SNMP @@ -26,13 +26,17 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Brother from a config entry.""" host = entry.data[CONF_HOST] - kind = entry.data[CONF_TYPE] + printer_type = entry.data[CONF_TYPE] snmp_engine = get_snmp_engine(hass) + try: + brother = await Brother.create( + host, printer_type=printer_type, snmp_engine=snmp_engine + ) + except (ConnectionError, SnmpError) as error: + raise ConfigEntryNotReady from error - coordinator = BrotherDataUpdateCoordinator( - hass, host=host, kind=kind, snmp_engine=snmp_engine - ) + coordinator = BrotherDataUpdateCoordinator(hass, brother) await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {}) @@ -61,11 +65,9 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: class BrotherDataUpdateCoordinator(DataUpdateCoordinator): """Class to manage fetching Brother data from the printer.""" - def __init__( - self, hass: HomeAssistant, host: str, kind: str, snmp_engine: SnmpEngine - ) -> None: + def __init__(self, hass: HomeAssistant, brother: Brother) -> None: """Initialize.""" - self.brother = Brother(host, kind=kind, snmp_engine=snmp_engine) + self.brother = brother super().__init__( hass, @@ -74,7 +76,7 @@ class BrotherDataUpdateCoordinator(DataUpdateCoordinator): update_interval=SCAN_INTERVAL, ) - async def _async_update_data(self) -> DictToObj: + async def _async_update_data(self) -> BrotherSensors: """Update data via library.""" try: async with async_timeout.timeout(20): diff --git a/homeassistant/components/brother/config_flow.py b/homeassistant/components/brother/config_flow.py index bcedc65d7ff..48c73d0c4d1 100644 --- a/homeassistant/components/brother/config_flow.py +++ b/homeassistant/components/brother/config_flow.py @@ -1,8 +1,6 @@ """Adds config flow for Brother Printer.""" from __future__ import annotations -import ipaddress -import re from typing import Any from brother import Brother, SnmpError, UnsupportedModel @@ -12,6 +10,7 @@ from homeassistant import config_entries, exceptions from homeassistant.components import zeroconf from homeassistant.const import CONF_HOST, CONF_TYPE from homeassistant.data_entry_flow import FlowResult +from homeassistant.util.network import is_host_valid from .const import DOMAIN, PRINTER_TYPES from .utils import get_snmp_engine @@ -24,17 +23,6 @@ DATA_SCHEMA = vol.Schema( ) -def host_valid(host: str) -> bool: - """Return True if hostname or IP address is valid.""" - try: - if ipaddress.ip_address(host).version in [4, 6]: - return True - except ValueError: - pass - disallowed = re.compile(r"[^a-zA-Z\d\-]") - return all(x and not disallowed.search(x) for x in host.split(".")) - - class BrotherConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Brother Printer.""" @@ -53,12 +41,14 @@ class BrotherConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if user_input is not None: try: - if not host_valid(user_input[CONF_HOST]): + if not is_host_valid(user_input[CONF_HOST]): raise InvalidHost() snmp_engine = get_snmp_engine(self.hass) - brother = Brother(user_input[CONF_HOST], snmp_engine=snmp_engine) + brother = await Brother.create( + user_input[CONF_HOST], snmp_engine=snmp_engine + ) await brother.async_update() await self.async_set_unique_id(brother.serial.lower()) @@ -92,7 +82,9 @@ class BrotherConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): model = discovery_info.properties.get("product") try: - self.brother = Brother(self.host, snmp_engine=snmp_engine, model=model) + self.brother = await Brother.create( + self.host, snmp_engine=snmp_engine, model=model + ) await self.brother.async_update() except UnsupportedModel: return self.async_abort(reason="unsupported_model") diff --git a/homeassistant/components/brother/diagnostics.py b/homeassistant/components/brother/diagnostics.py index 9ff515004cb..239d4916d6b 100644 --- a/homeassistant/components/brother/diagnostics.py +++ b/homeassistant/components/brother/diagnostics.py @@ -1,6 +1,8 @@ """Diagnostics support for Brother.""" from __future__ import annotations +from dataclasses import asdict + from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -18,7 +20,7 @@ async def async_get_config_entry_diagnostics( diagnostics_data = { "info": dict(config_entry.data), - "data": coordinator.data, + "data": asdict(coordinator.data), } return diagnostics_data diff --git a/homeassistant/components/brother/manifest.json b/homeassistant/components/brother/manifest.json index e14079f6dd9..61b1d8bcdc9 100644 --- a/homeassistant/components/brother/manifest.json +++ b/homeassistant/components/brother/manifest.json @@ -3,7 +3,7 @@ "name": "Brother Printer", "documentation": "https://www.home-assistant.io/integrations/brother", "codeowners": ["@bieniu"], - "requirements": ["brother==1.2.3"], + "requirements": ["brother==2.0.0"], "zeroconf": [ { "type": "_printer._tcp.local.", diff --git a/homeassistant/components/brother/sensor.py b/homeassistant/components/brother/sensor.py index b6af96087af..86f1d2d40ec 100644 --- a/homeassistant/components/brother/sensor.py +++ b/homeassistant/components/brother/sensor.py @@ -3,9 +3,11 @@ from __future__ import annotations from dataclasses import dataclass from datetime import datetime +import logging from typing import Any, cast from homeassistant.components.sensor import ( + DOMAIN as PLATFORM, SensorDeviceClass, SensorEntity, SensorEntityDescription, @@ -14,6 +16,7 @@ from homeassistant.components.sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, PERCENTAGE from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity import DeviceInfo, EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType @@ -28,7 +31,7 @@ ATTR_BLACK_DRUM_REMAINING_LIFE = "black_drum_remaining_life" ATTR_BLACK_DRUM_REMAINING_PAGES = "black_drum_remaining_pages" ATTR_BLACK_INK_REMAINING = "black_ink_remaining" ATTR_BLACK_TONER_REMAINING = "black_toner_remaining" -ATTR_BW_COUNTER = "b/w_counter" +ATTR_BW_COUNTER = "bw_counter" ATTR_COLOR_COUNTER = "color_counter" ATTR_COUNTER = "counter" ATTR_CYAN_DRUM_COUNTER = "cyan_drum_counter" @@ -82,6 +85,8 @@ ATTRS_MAP: dict[str, tuple[str, str]] = { ), } +_LOGGER = logging.getLogger(__name__) + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback @@ -89,6 +94,22 @@ async def async_setup_entry( """Add Brother entities from a config_entry.""" coordinator = hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id] + # Due to the change of the attribute name of one sensor, it is necessary to migrate + # the unique_id to the new one. + entity_registry = er.async_get(hass) + old_unique_id = f"{coordinator.data.serial.lower()}_b/w_counter" + if entity_id := entity_registry.async_get_entity_id( + PLATFORM, DOMAIN, old_unique_id + ): + new_unique_id = f"{coordinator.data.serial.lower()}_bw_counter" + _LOGGER.debug( + "Migrating entity %s from old unique ID '%s' to new unique ID '%s'", + entity_id, + old_unique_id, + new_unique_id, + ) + entity_registry.async_update_entity(entity_id, new_unique_id=new_unique_id) + sensors = [] device_info = DeviceInfo( @@ -97,11 +118,11 @@ async def async_setup_entry( manufacturer=ATTR_MANUFACTURER, model=coordinator.data.model, name=coordinator.data.model, - sw_version=getattr(coordinator.data, "firmware", None), + sw_version=coordinator.data.firmware, ) for description in SENSOR_TYPES: - if description.key in coordinator.data: + if getattr(coordinator.data, description.key) is not None: sensors.append( description.entity_class(coordinator, description, device_info) ) diff --git a/homeassistant/components/brunt/translations/cs.json b/homeassistant/components/brunt/translations/cs.json index 72df4a96818..e5d6edc65ea 100644 --- a/homeassistant/components/brunt/translations/cs.json +++ b/homeassistant/components/brunt/translations/cs.json @@ -1,11 +1,27 @@ { "config": { "abort": { + "already_configured": "\u00da\u010det je ji\u017e nastaven", "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" }, "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Heslo" + }, + "title": "Znovu ov\u011b\u0159it integraci" + }, + "user": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/brunt/translations/es.json b/homeassistant/components/brunt/translations/es.json index 7a912e267f9..e48edfeb3f0 100644 --- a/homeassistant/components/brunt/translations/es.json +++ b/homeassistant/components/brunt/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "error": { "cannot_connect": "No se pudo conectar", diff --git a/homeassistant/components/brunt/translations/pt.json b/homeassistant/components/brunt/translations/pt.json index 6f18afa4df3..8df574de6eb 100644 --- a/homeassistant/components/brunt/translations/pt.json +++ b/homeassistant/components/brunt/translations/pt.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Conta j\u00e1 configurada" + "already_configured": "Conta j\u00e1 configurada", + "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida" }, "error": { "cannot_connect": "Falha na liga\u00e7\u00e3o", @@ -10,11 +11,16 @@ }, "step": { "reauth_confirm": { + "data": { + "password": "Palavra-passe" + }, + "description": "Por favor, introduza novamente a palavra-passe para: {username}", "title": "Reautenticar integra\u00e7\u00e3o" }, "user": { "data": { - "password": "Palavra-passe" + "password": "Palavra-passe", + "username": "Nome de Utilizador" } } } diff --git a/homeassistant/components/bsblan/climate.py b/homeassistant/components/bsblan/climate.py index e83415ebf52..b7fa2bb8010 100644 --- a/homeassistant/components/bsblan/climate.py +++ b/homeassistant/components/bsblan/climate.py @@ -7,12 +7,12 @@ from typing import Any from bsblan import BSBLan, BSBLanError, Info, State -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( ATTR_HVAC_MODE, ATTR_PRESET_MODE, PRESET_ECO, PRESET_NONE, + ClimateEntity, ClimateEntityFeature, HVACMode, ) diff --git a/homeassistant/components/bsblan/translations/pt.json b/homeassistant/components/bsblan/translations/pt.json index 3cb7a7d5891..0b09e208858 100644 --- a/homeassistant/components/bsblan/translations/pt.json +++ b/homeassistant/components/bsblan/translations/pt.json @@ -7,6 +7,7 @@ "error": { "cannot_connect": "Falha na liga\u00e7\u00e3o" }, + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/bt_home_hub_5/device_tracker.py b/homeassistant/components/bt_home_hub_5/device_tracker.py index 2cef6e9ba41..4d89c851245 100644 --- a/homeassistant/components/bt_home_hub_5/device_tracker.py +++ b/homeassistant/components/bt_home_hub_5/device_tracker.py @@ -25,7 +25,9 @@ PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( ) -def get_scanner(hass: HomeAssistant, config: ConfigType) -> DeviceScanner | None: +def get_scanner( + hass: HomeAssistant, config: ConfigType +) -> BTHomeHub5DeviceScanner | None: """Return a BT Home Hub 5 scanner if successful.""" scanner = BTHomeHub5DeviceScanner(config[DOMAIN]) diff --git a/homeassistant/components/bt_smarthub/device_tracker.py b/homeassistant/components/bt_smarthub/device_tracker.py index 0d50b09affa..48475bbeac9 100644 --- a/homeassistant/components/bt_smarthub/device_tracker.py +++ b/homeassistant/components/bt_smarthub/device_tracker.py @@ -30,7 +30,7 @@ PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( ) -def get_scanner(hass: HomeAssistant, config: ConfigType) -> DeviceScanner | None: +def get_scanner(hass: HomeAssistant, config: ConfigType) -> BTSmartHubScanner | None: """Return a BT Smart Hub scanner if successful.""" info = config[DOMAIN] smarthub_client = BTSmartHub( diff --git a/homeassistant/components/bthome/__init__.py b/homeassistant/components/bthome/__init__.py index 93ebd7b288f..539aa112a06 100644 --- a/homeassistant/components/bthome/__init__.py +++ b/homeassistant/components/bthome/__init__.py @@ -19,7 +19,7 @@ from homeassistant.core import HomeAssistant from .const import DOMAIN -PLATFORMS: list[Platform] = [Platform.SENSOR] +PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.SENSOR] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/bthome/binary_sensor.py b/homeassistant/components/bthome/binary_sensor.py new file mode 100644 index 00000000000..a048f9202b6 --- /dev/null +++ b/homeassistant/components/bthome/binary_sensor.py @@ -0,0 +1,198 @@ +"""Support for BTHome binary sensors.""" +from __future__ import annotations + +from typing import Optional + +from bthome_ble import ( + BinarySensorDeviceClass as BTHomeBinarySensorDeviceClass, + SensorUpdate, +) + +from homeassistant import config_entries +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntity, + BinarySensorEntityDescription, +) +from homeassistant.components.bluetooth.passive_update_processor import ( + PassiveBluetoothDataProcessor, + PassiveBluetoothDataUpdate, + PassiveBluetoothProcessorCoordinator, + PassiveBluetoothProcessorEntity, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .device import device_key_to_bluetooth_entity_key, sensor_device_info_to_hass + +BINARY_SENSOR_DESCRIPTIONS = { + BTHomeBinarySensorDeviceClass.BATTERY: BinarySensorEntityDescription( + key=BTHomeBinarySensorDeviceClass.BATTERY, + device_class=BinarySensorDeviceClass.BATTERY, + ), + BTHomeBinarySensorDeviceClass.BATTERY_CHARGING: BinarySensorEntityDescription( + key=BTHomeBinarySensorDeviceClass.BATTERY_CHARGING, + device_class=BinarySensorDeviceClass.BATTERY_CHARGING, + ), + BTHomeBinarySensorDeviceClass.CO: BinarySensorEntityDescription( + key=BTHomeBinarySensorDeviceClass.CO, + device_class=BinarySensorDeviceClass.CO, + ), + BTHomeBinarySensorDeviceClass.COLD: BinarySensorEntityDescription( + key=BTHomeBinarySensorDeviceClass.COLD, + device_class=BinarySensorDeviceClass.COLD, + ), + BTHomeBinarySensorDeviceClass.CONNECTIVITY: BinarySensorEntityDescription( + key=BTHomeBinarySensorDeviceClass.CONNECTIVITY, + device_class=BinarySensorDeviceClass.CONNECTIVITY, + ), + BTHomeBinarySensorDeviceClass.DOOR: BinarySensorEntityDescription( + key=BTHomeBinarySensorDeviceClass.DOOR, + device_class=BinarySensorDeviceClass.DOOR, + ), + BTHomeBinarySensorDeviceClass.HEAT: BinarySensorEntityDescription( + key=BTHomeBinarySensorDeviceClass.HEAT, + device_class=BinarySensorDeviceClass.HEAT, + ), + BTHomeBinarySensorDeviceClass.GARAGE_DOOR: BinarySensorEntityDescription( + key=BTHomeBinarySensorDeviceClass.GARAGE_DOOR, + device_class=BinarySensorDeviceClass.GARAGE_DOOR, + ), + BTHomeBinarySensorDeviceClass.GAS: BinarySensorEntityDescription( + key=BTHomeBinarySensorDeviceClass.GAS, + device_class=BinarySensorDeviceClass.GAS, + ), + BTHomeBinarySensorDeviceClass.GENERIC: BinarySensorEntityDescription( + key=BTHomeBinarySensorDeviceClass.GENERIC, + ), + BTHomeBinarySensorDeviceClass.LIGHT: BinarySensorEntityDescription( + key=BTHomeBinarySensorDeviceClass.LIGHT, + device_class=BinarySensorDeviceClass.LIGHT, + ), + BTHomeBinarySensorDeviceClass.LOCK: BinarySensorEntityDescription( + key=BTHomeBinarySensorDeviceClass.LOCK, + device_class=BinarySensorDeviceClass.LOCK, + ), + BTHomeBinarySensorDeviceClass.MOISTURE: BinarySensorEntityDescription( + key=BTHomeBinarySensorDeviceClass.MOISTURE, + device_class=BinarySensorDeviceClass.MOISTURE, + ), + BTHomeBinarySensorDeviceClass.MOTION: BinarySensorEntityDescription( + key=BTHomeBinarySensorDeviceClass.MOTION, + device_class=BinarySensorDeviceClass.MOTION, + ), + BTHomeBinarySensorDeviceClass.MOVING: BinarySensorEntityDescription( + key=BTHomeBinarySensorDeviceClass.MOVING, + device_class=BinarySensorDeviceClass.MOVING, + ), + BTHomeBinarySensorDeviceClass.OCCUPANCY: BinarySensorEntityDescription( + key=BTHomeBinarySensorDeviceClass.OCCUPANCY, + device_class=BinarySensorDeviceClass.OCCUPANCY, + ), + BTHomeBinarySensorDeviceClass.OPENING: BinarySensorEntityDescription( + key=BTHomeBinarySensorDeviceClass.OPENING, + device_class=BinarySensorDeviceClass.OPENING, + ), + BTHomeBinarySensorDeviceClass.PLUG: BinarySensorEntityDescription( + key=BTHomeBinarySensorDeviceClass.PLUG, + device_class=BinarySensorDeviceClass.PLUG, + ), + BTHomeBinarySensorDeviceClass.POWER: BinarySensorEntityDescription( + key=BTHomeBinarySensorDeviceClass.POWER, + device_class=BinarySensorDeviceClass.POWER, + ), + BTHomeBinarySensorDeviceClass.PRESENCE: BinarySensorEntityDescription( + key=BTHomeBinarySensorDeviceClass.PRESENCE, + device_class=BinarySensorDeviceClass.PRESENCE, + ), + BTHomeBinarySensorDeviceClass.PROBLEM: BinarySensorEntityDescription( + key=BTHomeBinarySensorDeviceClass.PROBLEM, + device_class=BinarySensorDeviceClass.PROBLEM, + ), + BTHomeBinarySensorDeviceClass.RUNNING: BinarySensorEntityDescription( + key=BTHomeBinarySensorDeviceClass.RUNNING, + device_class=BinarySensorDeviceClass.RUNNING, + ), + BTHomeBinarySensorDeviceClass.SAFETY: BinarySensorEntityDescription( + key=BTHomeBinarySensorDeviceClass.SAFETY, + device_class=BinarySensorDeviceClass.SAFETY, + ), + BTHomeBinarySensorDeviceClass.SMOKE: BinarySensorEntityDescription( + key=BTHomeBinarySensorDeviceClass.SMOKE, + device_class=BinarySensorDeviceClass.SMOKE, + ), + BTHomeBinarySensorDeviceClass.SOUND: BinarySensorEntityDescription( + key=BTHomeBinarySensorDeviceClass.SOUND, + device_class=BinarySensorDeviceClass.SOUND, + ), + BTHomeBinarySensorDeviceClass.TAMPER: BinarySensorEntityDescription( + key=BTHomeBinarySensorDeviceClass.TAMPER, + device_class=BinarySensorDeviceClass.TAMPER, + ), + BTHomeBinarySensorDeviceClass.VIBRATION: BinarySensorEntityDescription( + key=BTHomeBinarySensorDeviceClass.VIBRATION, + device_class=BinarySensorDeviceClass.VIBRATION, + ), + BTHomeBinarySensorDeviceClass.WINDOW: BinarySensorEntityDescription( + key=BTHomeBinarySensorDeviceClass.WINDOW, + device_class=BinarySensorDeviceClass.WINDOW, + ), +} + + +def sensor_update_to_bluetooth_data_update( + sensor_update: SensorUpdate, +) -> PassiveBluetoothDataUpdate: + """Convert a binary sensor update to a bluetooth data update.""" + return PassiveBluetoothDataUpdate( + devices={ + device_id: sensor_device_info_to_hass(device_info) + for device_id, device_info in sensor_update.devices.items() + }, + entity_descriptions={ + device_key_to_bluetooth_entity_key(device_key): BINARY_SENSOR_DESCRIPTIONS[ + description.device_class + ] + for device_key, description in sensor_update.binary_entity_descriptions.items() + if description.device_class + }, + entity_data={ + device_key_to_bluetooth_entity_key(device_key): sensor_values.native_value + for device_key, sensor_values in sensor_update.binary_entity_values.items() + }, + entity_names={ + device_key_to_bluetooth_entity_key(device_key): sensor_values.name + for device_key, sensor_values in sensor_update.binary_entity_values.items() + }, + ) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: config_entries.ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the BTHome BLE binary 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( + BTHomeBluetoothBinarySensorEntity, async_add_entities + ) + ) + entry.async_on_unload(coordinator.async_register_processor(processor)) + + +class BTHomeBluetoothBinarySensorEntity( + PassiveBluetoothProcessorEntity[PassiveBluetoothDataProcessor[Optional[bool]]], + BinarySensorEntity, +): + """Representation of a BTHome binary sensor.""" + + @property + def is_on(self) -> bool | None: + """Return the native value.""" + return self.processor.entity_data.get(self.entity_key) diff --git a/homeassistant/components/bthome/manifest.json b/homeassistant/components/bthome/manifest.json index 597d52c72e4..3b4cbe2f4f4 100644 --- a/homeassistant/components/bthome/manifest.json +++ b/homeassistant/components/bthome/manifest.json @@ -13,7 +13,7 @@ "service_data_uuid": "0000181e-0000-1000-8000-00805f9b34fb" } ], - "requirements": ["bthome-ble==1.0.0"], + "requirements": ["bthome-ble==1.2.2"], "dependencies": ["bluetooth"], "codeowners": ["@Ernst79"], "iot_class": "local_push" diff --git a/homeassistant/components/bthome/sensor.py b/homeassistant/components/bthome/sensor.py index a0068596b01..9d68ce2d3b4 100644 --- a/homeassistant/components/bthome/sensor.py +++ b/homeassistant/components/bthome/sensor.py @@ -3,7 +3,7 @@ from __future__ import annotations from typing import Optional, Union -from bthome_ble import DeviceClass, SensorUpdate, Units +from bthome_ble import SensorDeviceClass as BTHomeSensorDeviceClass, SensorUpdate, Units from homeassistant import config_entries from homeassistant.components.bluetooth.passive_update_processor import ( @@ -40,93 +40,102 @@ from .const import DOMAIN from .device import device_key_to_bluetooth_entity_key, sensor_device_info_to_hass SENSOR_DESCRIPTIONS = { - (DeviceClass.TEMPERATURE, Units.TEMP_CELSIUS): SensorEntityDescription( - key=f"{DeviceClass.TEMPERATURE}_{Units.TEMP_CELSIUS}", + (BTHomeSensorDeviceClass.TEMPERATURE, Units.TEMP_CELSIUS): SensorEntityDescription( + key=f"{BTHomeSensorDeviceClass.TEMPERATURE}_{Units.TEMP_CELSIUS}", device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=TEMP_CELSIUS, state_class=SensorStateClass.MEASUREMENT, ), - (DeviceClass.HUMIDITY, Units.PERCENTAGE): SensorEntityDescription( - key=f"{DeviceClass.HUMIDITY}_{Units.PERCENTAGE}", + (BTHomeSensorDeviceClass.HUMIDITY, Units.PERCENTAGE): SensorEntityDescription( + key=f"{BTHomeSensorDeviceClass.HUMIDITY}_{Units.PERCENTAGE}", device_class=SensorDeviceClass.HUMIDITY, native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, ), - (DeviceClass.ILLUMINANCE, Units.LIGHT_LUX): SensorEntityDescription( - key=f"{DeviceClass.ILLUMINANCE}_{Units.LIGHT_LUX}", + (BTHomeSensorDeviceClass.ILLUMINANCE, Units.LIGHT_LUX): SensorEntityDescription( + key=f"{BTHomeSensorDeviceClass.ILLUMINANCE}_{Units.LIGHT_LUX}", device_class=SensorDeviceClass.ILLUMINANCE, native_unit_of_measurement=LIGHT_LUX, state_class=SensorStateClass.MEASUREMENT, ), - (DeviceClass.PRESSURE, Units.PRESSURE_MBAR): SensorEntityDescription( - key=f"{DeviceClass.PRESSURE}_{Units.PRESSURE_MBAR}", + (BTHomeSensorDeviceClass.PRESSURE, Units.PRESSURE_MBAR): SensorEntityDescription( + key=f"{BTHomeSensorDeviceClass.PRESSURE}_{Units.PRESSURE_MBAR}", device_class=SensorDeviceClass.PRESSURE, native_unit_of_measurement=PRESSURE_MBAR, state_class=SensorStateClass.MEASUREMENT, ), - (DeviceClass.BATTERY, Units.PERCENTAGE): SensorEntityDescription( - key=f"{DeviceClass.BATTERY}_{Units.PERCENTAGE}", + (BTHomeSensorDeviceClass.BATTERY, Units.PERCENTAGE): SensorEntityDescription( + key=f"{BTHomeSensorDeviceClass.BATTERY}_{Units.PERCENTAGE}", device_class=SensorDeviceClass.BATTERY, native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, ), - (DeviceClass.VOLTAGE, Units.ELECTRIC_POTENTIAL_VOLT): SensorEntityDescription( - key=str(Units.ELECTRIC_POTENTIAL_VOLT), + ( + BTHomeSensorDeviceClass.VOLTAGE, + Units.ELECTRIC_POTENTIAL_VOLT, + ): SensorEntityDescription( + key=f"{BTHomeSensorDeviceClass.VOLTAGE}_{Units.ELECTRIC_POTENTIAL_VOLT}", device_class=SensorDeviceClass.VOLTAGE, native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, state_class=SensorStateClass.MEASUREMENT, ), - (DeviceClass.ENERGY, Units.ENERGY_KILO_WATT_HOUR): SensorEntityDescription( - key=str(Units.ENERGY_KILO_WATT_HOUR), + ( + BTHomeSensorDeviceClass.ENERGY, + Units.ENERGY_KILO_WATT_HOUR, + ): SensorEntityDescription( + key=f"{BTHomeSensorDeviceClass.ENERGY}_{Units.ENERGY_KILO_WATT_HOUR}", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, state_class=SensorStateClass.TOTAL_INCREASING, ), - (DeviceClass.POWER, Units.POWER_WATT): SensorEntityDescription( - key=str(Units.POWER_WATT), + (BTHomeSensorDeviceClass.POWER, Units.POWER_WATT): SensorEntityDescription( + key=f"{BTHomeSensorDeviceClass.POWER}_{Units.POWER_WATT}", device_class=SensorDeviceClass.POWER, native_unit_of_measurement=POWER_WATT, state_class=SensorStateClass.MEASUREMENT, ), ( - DeviceClass.PM10, + BTHomeSensorDeviceClass.PM10, Units.CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, ): SensorEntityDescription( - key=f"{DeviceClass.PM10}_{Units.CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}", + key=f"{BTHomeSensorDeviceClass.PM10}_{Units.CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}", device_class=SensorDeviceClass.PM10, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, state_class=SensorStateClass.MEASUREMENT, ), ( - DeviceClass.PM25, + BTHomeSensorDeviceClass.PM25, Units.CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, ): SensorEntityDescription( - key=f"{DeviceClass.PM25}_{Units.CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}", + key=f"{BTHomeSensorDeviceClass.PM25}_{Units.CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}", device_class=SensorDeviceClass.PM25, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, state_class=SensorStateClass.MEASUREMENT, ), - (DeviceClass.CO2, Units.CONCENTRATION_PARTS_PER_MILLION,): SensorEntityDescription( - key=f"{DeviceClass.CO2}_{Units.CONCENTRATION_PARTS_PER_MILLION}", + ( + BTHomeSensorDeviceClass.CO2, + Units.CONCENTRATION_PARTS_PER_MILLION, + ): SensorEntityDescription( + key=f"{BTHomeSensorDeviceClass.CO2}_{Units.CONCENTRATION_PARTS_PER_MILLION}", device_class=SensorDeviceClass.CO2, native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, state_class=SensorStateClass.MEASUREMENT, ), ( - DeviceClass.VOLATILE_ORGANIC_COMPOUNDS, + BTHomeSensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS, Units.CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, ): SensorEntityDescription( - key=f"{DeviceClass.VOLATILE_ORGANIC_COMPOUNDS}_{Units.CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}", + key=f"{BTHomeSensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS}_{Units.CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}", device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, state_class=SensorStateClass.MEASUREMENT, ), ( - DeviceClass.SIGNAL_STRENGTH, + BTHomeSensorDeviceClass.SIGNAL_STRENGTH, Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ): SensorEntityDescription( - key=f"{DeviceClass.SIGNAL_STRENGTH}_{Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT}", + key=f"{BTHomeSensorDeviceClass.SIGNAL_STRENGTH}_{Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT}", device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, state_class=SensorStateClass.MEASUREMENT, @@ -134,36 +143,36 @@ SENSOR_DESCRIPTIONS = { entity_registry_enabled_default=False, ), # Used for mass sensor with kg unit - (None, Units.MASS_KILOGRAMS): SensorEntityDescription( - key=f"{DeviceClass.MASS}_{Units.MASS_KILOGRAMS}", - device_class=None, + (BTHomeSensorDeviceClass.MASS, Units.MASS_KILOGRAMS): SensorEntityDescription( + key=f"{BTHomeSensorDeviceClass.MASS}_{Units.MASS_KILOGRAMS}", + device_class=SensorDeviceClass.WEIGHT, native_unit_of_measurement=MASS_KILOGRAMS, state_class=SensorStateClass.MEASUREMENT, ), # Used for mass sensor with lb unit - (None, Units.MASS_POUNDS): SensorEntityDescription( - key=f"{DeviceClass.MASS}_{Units.MASS_POUNDS}", - device_class=None, + (BTHomeSensorDeviceClass.MASS, Units.MASS_POUNDS): SensorEntityDescription( + key=f"{BTHomeSensorDeviceClass.MASS}_{Units.MASS_POUNDS}", + device_class=SensorDeviceClass.WEIGHT, native_unit_of_measurement=MASS_POUNDS, state_class=SensorStateClass.MEASUREMENT, ), # Used for moisture sensor - (None, Units.PERCENTAGE,): SensorEntityDescription( - key=f"{DeviceClass.MOISTURE}_{Units.PERCENTAGE}", - device_class=None, + (BTHomeSensorDeviceClass.MOISTURE, Units.PERCENTAGE): SensorEntityDescription( + key=f"{BTHomeSensorDeviceClass.MOISTURE}_{Units.PERCENTAGE}", + device_class=SensorDeviceClass.MOISTURE, native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, ), # Used for dew point sensor - (None, Units.TEMP_CELSIUS): SensorEntityDescription( - key=f"{DeviceClass.DEW_POINT}_{Units.TEMP_CELSIUS}", + (BTHomeSensorDeviceClass.DEW_POINT, Units.TEMP_CELSIUS): SensorEntityDescription( + key=f"{BTHomeSensorDeviceClass.DEW_POINT}_{Units.TEMP_CELSIUS}", device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=TEMP_CELSIUS, state_class=SensorStateClass.MEASUREMENT, ), # Used for count sensor - (None, None): SensorEntityDescription( - key=f"{DeviceClass.COUNT}", + (BTHomeSensorDeviceClass.COUNT, None): SensorEntityDescription( + key=f"{BTHomeSensorDeviceClass.COUNT}", device_class=None, native_unit_of_measurement=None, state_class=SensorStateClass.MEASUREMENT, @@ -185,6 +194,7 @@ def sensor_update_to_bluetooth_data_update( (description.device_class, description.native_unit_of_measurement) ] for device_key, description in sensor_update.entity_descriptions.items() + if description.device_class }, entity_data={ device_key_to_bluetooth_entity_key(device_key): sensor_values.native_value diff --git a/homeassistant/components/bthome/translations/bg.json b/homeassistant/components/bthome/translations/bg.json new file mode 100644 index 00000000000..895fcac7c4f --- /dev/null +++ b/homeassistant/components/bthome/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", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" + }, + "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/bthome/translations/cs.json b/homeassistant/components/bthome/translations/cs.json new file mode 100644 index 00000000000..76d1e332913 --- /dev/null +++ b/homeassistant/components/bthome/translations/cs.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "already_in_progress": "Konfigurace ji\u017e prob\u00edh\u00e1", + "no_devices_found": "V s\u00edti nebyla nalezena \u017e\u00e1dn\u00e1 za\u0159\u00edzen\u00ed", + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Chcete nastavit {name}?" + }, + "user": { + "data": { + "address": "Za\u0159\u00edzen\u00ed" + }, + "description": "Zvolte za\u0159\u00edzen\u00ed, kter\u00e9 chcete nastavit" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bthome/translations/es.json b/homeassistant/components/bthome/translations/es.json index 4cf15c9c1d3..ed86afea60b 100644 --- a/homeassistant/components/bthome/translations/es.json +++ b/homeassistant/components/bthome/translations/es.json @@ -4,7 +4,7 @@ "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", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "error": { "decryption_failed": "La clave de enlace proporcionada no funcion\u00f3, los datos del sensor no se pudieron descifrar. Por favor, compru\u00e9balo e int\u00e9ntalo de nuevo.", diff --git a/homeassistant/components/bthome/translations/he.json b/homeassistant/components/bthome/translations/he.json new file mode 100644 index 00000000000..47308062d0d --- /dev/null +++ b/homeassistant/components/bthome/translations/he.json @@ -0,0 +1,16 @@ +{ + "config": { + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea {name}?" + }, + "user": { + "data": { + "address": "\u05d4\u05ea\u05e7\u05df" + }, + "description": "\u05d1\u05d7\u05d9\u05e8\u05ea \u05d4\u05ea\u05e7\u05df \u05dc\u05d4\u05d2\u05d3\u05e8\u05d4" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bthome/translations/hu.json b/homeassistant/components/bthome/translations/hu.json new file mode 100644 index 00000000000..1bf4fffab68 --- /dev/null +++ b/homeassistant/components/bthome/translations/hu.json @@ -0,0 +1,32 @@ +{ + "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", + "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.", + "expected_32_characters": "32 karakterb\u0151l \u00e1ll\u00f3 hexadecim\u00e1lis kulcsra van sz\u00fcks\u00e9g." + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: {name}?" + }, + "get_encryption_key": { + "data": { + "bindkey": "Kulcs (bindkey)" + }, + "description": "Az \u00e9rz\u00e9kel\u0151 adatai titkos\u00edtva vannak. A visszafejt\u00e9shez egy 32 karakterb\u0151l \u00e1ll\u00f3 hexadecim\u00e1lis kulcsra van sz\u00fcks\u00e9g." + }, + "user": { + "data": { + "address": "Eszk\u00f6z" + }, + "description": "V\u00e1lassza ki a be\u00e1ll\u00edtani k\u00edv\u00e1nt eszk\u00f6zt" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bthome/translations/nl.json b/homeassistant/components/bthome/translations/nl.json index 9a4e727ef2e..6b79e0311de 100644 --- a/homeassistant/components/bthome/translations/nl.json +++ b/homeassistant/components/bthome/translations/nl.json @@ -12,6 +12,9 @@ "description": "Wilt u {name} instellen?" }, "user": { + "data": { + "address": "Apparaat" + }, "description": "Kies een apparaat om in te stellen" } } diff --git a/homeassistant/components/bthome/translations/pl.json b/homeassistant/components/bthome/translations/pl.json index 814c4cd9757..db19044b347 100644 --- a/homeassistant/components/bthome/translations/pl.json +++ b/homeassistant/components/bthome/translations/pl.json @@ -6,11 +6,21 @@ "no_devices_found": "Nie znaleziono urz\u0105dze\u0144 w sieci", "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" }, + "error": { + "decryption_failed": "Podany klucz (bindkey) nie zadzia\u0142a\u0142, dane czujnika nie mog\u0142y zosta\u0107 odszyfrowane. Sprawd\u017a go i spr\u00f3buj ponownie.", + "expected_32_characters": "Oczekiwano 32-znakowego szesnastkowego klucza bindkey." + }, "flow_title": "{name}", "step": { "bluetooth_confirm": { "description": "Czy chcesz skonfigurowa\u0107 {name}?" }, + "get_encryption_key": { + "data": { + "bindkey": "Bindkey" + }, + "description": "Dane przesy\u0142ane przez sensor s\u0105 szyfrowane. Aby je odszyfrowa\u0107, potrzebujemy 32-znakowego szesnastkowego klucza bindkey." + }, "user": { "data": { "address": "Urz\u0105dzenie" diff --git a/homeassistant/components/bthome/translations/pt.json b/homeassistant/components/bthome/translations/pt.json new file mode 100644 index 00000000000..0294a42b129 --- /dev/null +++ b/homeassistant/components/bthome/translations/pt.json @@ -0,0 +1,22 @@ +{ + "config": { + "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", + "no_devices_found": "Nenhum dispositivo encontrado na rede", + "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida" + }, + "error": { + "decryption_failed": "A chave de liga\u00e7\u00e3o fornecida n\u00e3o funcionou, os dados do sensor n\u00e3o puderam ser descriptografados. Por favor verifique e tente novamente.", + "expected_32_characters": "Esperava-se uma chave de liga\u00e7\u00e3o hexadecimal de 32 caracteres." + }, + "step": { + "get_encryption_key": { + "data": { + "bindkey": "Chave de liga\u00e7\u00e3o" + }, + "description": "Os dados do sensor transmitidos pelo sensor s\u00e3o criptografados. Para decifr\u00e1-lo, precisamos de uma chave de liga\u00e7\u00e3o hexadecimal de 32 caracteres." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bthome/translations/sv.json b/homeassistant/components/bthome/translations/sv.json new file mode 100644 index 00000000000..d7ff3b69339 --- /dev/null +++ b/homeassistant/components/bthome/translations/sv.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "already_in_progress": "Konfigurationsfl\u00f6det p\u00e5g\u00e5r redan", + "no_devices_found": "Inga enheter hittades i n\u00e4tverket", + "reauth_successful": "\u00c5terautentisering lyckades" + }, + "error": { + "decryption_failed": "Den tillhandah\u00e5llna bindningsnyckeln fungerade inte, sensordata kunde inte dekrypteras. Kontrollera den och f\u00f6rs\u00f6k igen.", + "expected_32_characters": "F\u00f6rv\u00e4ntade ett hexadecimalt bindningsnyckel med 32 tecken." + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Vill du konfigurera {name}?" + }, + "get_encryption_key": { + "data": { + "bindkey": "Bindningsnyckel" + }, + "description": "De sensordata som s\u00e4nds av sensorn \u00e4r krypterade. F\u00f6r att dekryptera dem beh\u00f6ver vi en hexadecimal bindningsnyckel med 32 tecken." + }, + "user": { + "data": { + "address": "Enhet" + }, + "description": "V\u00e4lj en enhet att konfigurera" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bthome/translations/tr.json b/homeassistant/components/bthome/translations/tr.json new file mode 100644 index 00000000000..48b91fe6932 --- /dev/null +++ b/homeassistant/components/bthome/translations/tr.json @@ -0,0 +1,32 @@ +{ + "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", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" + }, + "error": { + "decryption_failed": "Sa\u011flanan ba\u011flama anahtar\u0131 \u00e7al\u0131\u015fmad\u0131, sens\u00f6r verilerinin \u015fifresi \u00e7\u00f6z\u00fclemedi. L\u00fctfen kontrol edin ve tekrar deneyin.", + "expected_32_characters": "32 karakterlik onalt\u0131l\u0131k bir ba\u011flama anahtar\u0131 bekleniyor." + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "{name} kurulumunu yapmak istiyor musunuz?" + }, + "get_encryption_key": { + "data": { + "bindkey": "Bindkey" + }, + "description": "Sens\u00f6r taraf\u0131ndan yay\u0131nlanan sens\u00f6r verileri \u015fifrelenmi\u015ftir. \u015eifreyi \u00e7\u00f6zmek i\u00e7in 32 karakterlik onalt\u0131l\u0131k bir ba\u011flama anahtar\u0131na ihtiyac\u0131m\u0131z var." + }, + "user": { + "data": { + "address": "Cihaz" + }, + "description": "Kurulum i\u00e7in bir cihaz se\u00e7in" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/buienradar/sensor.py b/homeassistant/components/buienradar/sensor.py index 2f3e60b0646..279fdc145d5 100644 --- a/homeassistant/components/buienradar/sensor.py +++ b/homeassistant/components/buienradar/sensor.py @@ -138,6 +138,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key="windspeed", name="Wind speed", native_unit_of_measurement=SPEED_KILOMETERS_PER_HOUR, + device_class=SensorDeviceClass.SPEED, icon="mdi:weather-windy", state_class=SensorStateClass.MEASUREMENT, ), @@ -169,12 +170,14 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key="visibility", name="Visibility", native_unit_of_measurement=LENGTH_KILOMETERS, + device_class=SensorDeviceClass.DISTANCE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="windgust", name="Wind gust", native_unit_of_measurement=SPEED_KILOMETERS_PER_HOUR, + device_class=SensorDeviceClass.SPEED, icon="mdi:weather-windy", ), SensorEntityDescription( @@ -463,30 +466,35 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key="windspeed_1d", name="Wind speed 1d", native_unit_of_measurement=SPEED_KILOMETERS_PER_HOUR, + device_class=SensorDeviceClass.SPEED, icon="mdi:weather-windy", ), SensorEntityDescription( key="windspeed_2d", name="Wind speed 2d", native_unit_of_measurement=SPEED_KILOMETERS_PER_HOUR, + device_class=SensorDeviceClass.SPEED, icon="mdi:weather-windy", ), SensorEntityDescription( key="windspeed_3d", name="Wind speed 3d", native_unit_of_measurement=SPEED_KILOMETERS_PER_HOUR, + device_class=SensorDeviceClass.SPEED, icon="mdi:weather-windy", ), SensorEntityDescription( key="windspeed_4d", name="Wind speed 4d", native_unit_of_measurement=SPEED_KILOMETERS_PER_HOUR, + device_class=SensorDeviceClass.SPEED, icon="mdi:weather-windy", ), SensorEntityDescription( key="windspeed_5d", name="Wind speed 5d", native_unit_of_measurement=SPEED_KILOMETERS_PER_HOUR, + device_class=SensorDeviceClass.SPEED, icon="mdi:weather-windy", ), SensorEntityDescription( diff --git a/homeassistant/components/buienradar/translations/pt.json b/homeassistant/components/buienradar/translations/pt.json index 2e6515edd09..7d614a9f841 100644 --- a/homeassistant/components/buienradar/translations/pt.json +++ b/homeassistant/components/buienradar/translations/pt.json @@ -1,8 +1,15 @@ { "config": { + "abort": { + "already_configured": "A localiza\u00e7\u00e3o j\u00e1 est\u00e1 configurada" + }, + "error": { + "already_configured": "A localiza\u00e7\u00e3o j\u00e1 est\u00e1 configurada" + }, "step": { "user": { "data": { + "latitude": "Latitude", "longitude": "Longitude" } } diff --git a/homeassistant/components/button/__init__.py b/homeassistant/components/button/__init__.py index d0e27662d41..99fe02f7a9d 100644 --- a/homeassistant/components/button/__init__.py +++ b/homeassistant/components/button/__init__.py @@ -41,10 +41,12 @@ class ButtonDeviceClass(StrEnum): DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.Coerce(ButtonDeviceClass)) +# mypy: disallow-any-generics + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up Button entities.""" - component = hass.data[DOMAIN] = EntityComponent( + component = hass.data[DOMAIN] = EntityComponent[ButtonEntity]( _LOGGER, DOMAIN, hass, SCAN_INTERVAL ) await component.async_setup(config) @@ -60,13 +62,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry.""" - component: EntityComponent = hass.data[DOMAIN] + component: EntityComponent[ButtonEntity] = hass.data[DOMAIN] return await component.async_setup_entry(entry) async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - component: EntityComponent = hass.data[DOMAIN] + component: EntityComponent[ButtonEntity] = hass.data[DOMAIN] return await component.async_unload_entry(entry) diff --git a/homeassistant/components/button/manifest.json b/homeassistant/components/button/manifest.json index beeaca487a6..02945d979ff 100644 --- a/homeassistant/components/button/manifest.json +++ b/homeassistant/components/button/manifest.json @@ -3,5 +3,6 @@ "name": "Button", "documentation": "https://www.home-assistant.io/integrations/button", "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "entity" } diff --git a/homeassistant/components/calendar/__init__.py b/homeassistant/components/calendar/__init__.py index 54da2a1cb02..cfbe038c251 100644 --- a/homeassistant/components/calendar/__init__.py +++ b/homeassistant/components/calendar/__init__.py @@ -32,10 +32,12 @@ DOMAIN = "calendar" ENTITY_ID_FORMAT = DOMAIN + ".{}" SCAN_INTERVAL = datetime.timedelta(seconds=60) +# mypy: disallow-any-generics + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Track states and offer events for calendars.""" - component = hass.data[DOMAIN] = EntityComponent( + component = hass.data[DOMAIN] = EntityComponent[CalendarEntity]( _LOGGER, DOMAIN, hass, SCAN_INTERVAL ) @@ -52,13 +54,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry.""" - component: EntityComponent = hass.data[DOMAIN] + component: EntityComponent[CalendarEntity] = hass.data[DOMAIN] return await component.async_setup_entry(entry) async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - component: EntityComponent = hass.data[DOMAIN] + component: EntityComponent[CalendarEntity] = hass.data[DOMAIN] return await component.async_unload_entry(entry) @@ -189,71 +191,6 @@ def is_offset_reached( return start + offset_time <= dt.now(start.tzinfo) -class CalendarEventDevice(Entity): - """Legacy API for calendar event entities.""" - - def __init_subclass__(cls, **kwargs: Any) -> None: - """Print deprecation warning.""" - super().__init_subclass__(**kwargs) - _LOGGER.warning( - "CalendarEventDevice is deprecated, modify %s to extend CalendarEntity", - cls.__name__, - ) - - @property - def event(self) -> dict[str, Any] | None: - """Return the next upcoming event.""" - raise NotImplementedError() - - @final - @property - def state_attributes(self) -> dict[str, Any] | None: - """Return the entity state attributes.""" - - if (event := self.event) is None: - return None - - event = normalize_event(event) - return { - "message": event["message"], - "all_day": event["all_day"], - "start_time": event["start"], - "end_time": event["end"], - "location": event["location"], - "description": event["description"], - } - - @final - @property - def state(self) -> str: - """Return the state of the calendar event.""" - if (event := self.event) is None: - return STATE_OFF - - event = normalize_event(event) - start = event["dt_start"] - end = event["dt_end"] - - if start is None or end is None: - return STATE_OFF - - now = dt.now() - - if start <= now < end: - return STATE_ON - - return STATE_OFF - - async def async_get_events( - self, - hass: HomeAssistant, - start_date: datetime.datetime, - end_date: datetime.datetime, - ) -> list[dict[str, Any]]: - """Return calendar events within a datetime range.""" - raise NotImplementedError() - - class CalendarEntity(Entity): """Base class for calendar event entities.""" @@ -308,16 +245,20 @@ class CalendarEventView(http.HomeAssistantView): url = "/api/calendars/{entity_id}" name = "api:calendars:calendar" - def __init__(self, component: EntityComponent) -> None: + def __init__(self, component: EntityComponent[CalendarEntity]) -> None: """Initialize calendar view.""" self.component = component async def get(self, request: web.Request, entity_id: str) -> web.Response: """Return calendar events.""" - entity = self.component.get_entity(entity_id) + if not (entity := self.component.get_entity(entity_id)) or not isinstance( + entity, CalendarEntity + ): + return web.Response(status=HTTPStatus.BAD_REQUEST) + start = request.query.get("start") end = request.query.get("end") - if start is None or end is None or entity is None: + if start is None or end is None: return web.Response(status=HTTPStatus.BAD_REQUEST) try: start_date = dt.parse_datetime(start) @@ -327,16 +268,6 @@ class CalendarEventView(http.HomeAssistantView): if start_date is None or end_date is None: return web.Response(status=HTTPStatus.BAD_REQUEST) - # Compatibility shim for old API - if isinstance(entity, CalendarEventDevice): - event_list = await entity.async_get_events( - request.app["hass"], start_date, end_date - ) - return self.json(event_list) - - if not isinstance(entity, CalendarEntity): - return web.Response(status=HTTPStatus.BAD_REQUEST) - try: calendar_event_list = await entity.async_get_events( request.app["hass"], start_date, end_date @@ -365,7 +296,7 @@ class CalendarListView(http.HomeAssistantView): url = "/api/calendars" name = "api:calendars" - def __init__(self, component: EntityComponent) -> None: + def __init__(self, component: EntityComponent[CalendarEntity]) -> None: """Initialize calendar view.""" self.component = component diff --git a/homeassistant/components/calendar/manifest.json b/homeassistant/components/calendar/manifest.json index 2fb4df84414..cc4f09cfa64 100644 --- a/homeassistant/components/calendar/manifest.json +++ b/homeassistant/components/calendar/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/calendar", "dependencies": ["http"], "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "entity" } diff --git a/homeassistant/components/calendar/trigger.py b/homeassistant/components/calendar/trigger.py index 74be0f7e71d..0fdb7259c9d 100644 --- a/homeassistant/components/calendar/trigger.py +++ b/homeassistant/components/calendar/trigger.py @@ -38,6 +38,8 @@ TRIGGER_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend( } ) +# mypy: disallow-any-generics + class CalendarEventListener: """Helper class to listen to calendar events.""" @@ -172,7 +174,7 @@ async def async_attach_trigger( event_type = config[CONF_EVENT] offset = config[CONF_OFFSET] - component: EntityComponent = hass.data[DOMAIN] + component: EntityComponent[CalendarEntity] = hass.data[DOMAIN] if not (entity := component.get_entity(entity_id)) or not isinstance( entity, CalendarEntity ): diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 5aa348c9fb8..fa807dd1440 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -21,7 +21,7 @@ import voluptuous as vol from homeassistant.components import websocket_api from homeassistant.components.http import KEY_AUTHENTICATED, HomeAssistantView -from homeassistant.components.media_player.const import ( +from homeassistant.components.media_player import ( ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_TYPE, DOMAIN as DOMAIN_MP, @@ -65,6 +65,8 @@ from .const import ( # noqa: F401 DATA_CAMERA_PREFS, DATA_RTSP_TO_WEB_RTC, DOMAIN, + PREF_ORIENTATION, + PREF_PRELOAD_STREAM, SERVICE_RECORD, STREAM_TYPE_HLS, STREAM_TYPE_WEB_RTC, @@ -73,8 +75,6 @@ from .const import ( # noqa: F401 from .img_util import scale_jpeg_camera_image from .prefs import CameraPreferences -# mypy: allow-untyped-calls - _LOGGER = logging.getLogger(__name__) SERVICE_ENABLE_MOTION: Final = "enable_motion_detection" @@ -322,12 +322,9 @@ def async_register_rtsp_to_web_rtc_provider( async def _async_refresh_providers(hass: HomeAssistant) -> None: """Check all cameras for any state changes for registered providers.""" - component: EntityComponent = hass.data[DOMAIN] + component: EntityComponent[Camera] = hass.data[DOMAIN] await asyncio.gather( - *( - cast(Camera, camera).async_refresh_providers() - for camera in component.entities - ) + *(camera.async_refresh_providers() for camera in component.entities) ) @@ -343,7 +340,7 @@ def _async_get_rtsp_to_web_rtc_providers( async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the camera component.""" - component = hass.data[DOMAIN] = EntityComponent( + component = hass.data[DOMAIN] = EntityComponent[Camera]( _LOGGER, DOMAIN, hass, SCAN_INTERVAL ) @@ -363,7 +360,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def preload_stream(_event: Event) -> None: for camera in component.entities: - camera = cast(Camera, camera) camera_prefs = prefs.get(camera.entity_id) if not camera_prefs.preload_stream: continue @@ -380,7 +376,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: def update_tokens(time: datetime) -> None: """Update tokens of the entities.""" for entity in component.entities: - entity = cast(Camera, entity) entity.async_update_token() entity.async_write_ha_state() @@ -411,13 +406,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry.""" - component: EntityComponent = hass.data[DOMAIN] + component: EntityComponent[Camera] = hass.data[DOMAIN] return await component.async_setup_entry(entry) async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - component: EntityComponent = hass.data[DOMAIN] + component: EntityComponent[Camera] = hass.data[DOMAIN] return await component.async_unload_entry(entry) @@ -698,7 +693,7 @@ class CameraView(HomeAssistantView): requires_auth = False - def __init__(self, component: EntityComponent) -> None: + def __init__(self, component: EntityComponent[Camera]) -> None: """Initialize a basic camera view.""" self.component = component @@ -707,8 +702,6 @@ class CameraView(HomeAssistantView): if (camera := self.component.get_entity(entity_id)) is None: raise web.HTTPNotFound() - camera = cast(Camera, camera) - authenticated = ( request[KEY_AUTHENTICATED] or request.query.get("token") in camera.access_tokens @@ -874,7 +867,8 @@ async def websocket_get_prefs( { vol.Required("type"): "camera/update_prefs", vol.Required("entity_id"): cv.entity_id, - vol.Optional("preload_stream"): bool, + vol.Optional(PREF_PRELOAD_STREAM): bool, + vol.Optional(PREF_ORIENTATION): vol.All(int, vol.Range(min=1, max=8)), } ) @websocket_api.async_response @@ -888,9 +882,13 @@ async def websocket_update_prefs( changes.pop("id") changes.pop("type") entity_id = changes.pop("entity_id") - await prefs.async_update(entity_id, **changes) - - connection.send_result(msg["id"], prefs.get(entity_id).as_dict()) + try: + entity_prefs = await prefs.async_update(entity_id, **changes) + except HomeAssistantError as ex: + _LOGGER.error("Error setting camera preferences: %s", ex) + connection.send_error(msg["id"], "update_failed", str(ex)) + else: + connection.send_result(msg["id"], entity_prefs) async def async_handle_snapshot_service( @@ -959,6 +957,7 @@ async def _async_stream_endpoint_url( # Update keepalive setting which manages idle shutdown camera_prefs = hass.data[DATA_CAMERA_PREFS].get(camera.entity_id) stream.keepalive = camera_prefs.preload_stream + stream.orientation = camera_prefs.orientation stream.add_provider(fmt) await stream.start() diff --git a/homeassistant/components/camera/const.py b/homeassistant/components/camera/const.py index fafed8a4266..ab5832e48ab 100644 --- a/homeassistant/components/camera/const.py +++ b/homeassistant/components/camera/const.py @@ -9,6 +9,7 @@ DATA_CAMERA_PREFS: Final = "camera_prefs" DATA_RTSP_TO_WEB_RTC: Final = "rtsp_to_web_rtc" PREF_PRELOAD_STREAM: Final = "preload_stream" +PREF_ORIENTATION: Final = "orientation" SERVICE_RECORD: Final = "record" diff --git a/homeassistant/components/camera/manifest.json b/homeassistant/components/camera/manifest.json index b1ab479f3a5..92bed21c1b8 100644 --- a/homeassistant/components/camera/manifest.json +++ b/homeassistant/components/camera/manifest.json @@ -6,5 +6,6 @@ "requirements": ["PyTurboJPEG==1.6.7"], "after_dependencies": ["media_player"], "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "entity" } diff --git a/homeassistant/components/camera/media_source.py b/homeassistant/components/camera/media_source.py index 733efb3a430..e386e864ded 100644 --- a/homeassistant/components/camera/media_source.py +++ b/homeassistant/components/camera/media_source.py @@ -1,13 +1,7 @@ """Expose cameras as media sources.""" from __future__ import annotations -from typing import Optional, cast - -from homeassistant.components.media_player.const import ( - MEDIA_CLASS_APP, - MEDIA_CLASS_VIDEO, -) -from homeassistant.components.media_player.errors import BrowseError +from homeassistant.components.media_player import BrowseError, MediaClass from homeassistant.components.media_source.error import Unresolvable from homeassistant.components.media_source.models import ( BrowseMediaSource, @@ -41,8 +35,8 @@ class CameraMediaSource(MediaSource): async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia: """Resolve media to a url.""" - component: EntityComponent = self.hass.data[DOMAIN] - camera = cast(Optional[Camera], component.get_entity(item.identifier)) + component: EntityComponent[Camera] = self.hass.data[DOMAIN] + camera = component.get_entity(item.identifier) if not camera: raise Unresolvable(f"Could not resolve media item: {item.identifier}") @@ -76,11 +70,10 @@ class CameraMediaSource(MediaSource): can_stream_hls = "stream" in self.hass.config.components # Root. List cameras. - component: EntityComponent = self.hass.data[DOMAIN] + component: EntityComponent[Camera] = self.hass.data[DOMAIN] children = [] not_shown = 0 for camera in component.entities: - camera = cast(Camera, camera) stream_type = camera.frontend_stream_type if stream_type is None: @@ -97,7 +90,7 @@ class CameraMediaSource(MediaSource): BrowseMediaSource( domain=DOMAIN, identifier=camera.entity_id, - media_class=MEDIA_CLASS_VIDEO, + media_class=MediaClass.VIDEO, media_content_type=content_type, title=camera.name, thumbnail=f"/api/camera_proxy/{camera.entity_id}", @@ -109,12 +102,12 @@ class CameraMediaSource(MediaSource): return BrowseMediaSource( domain=DOMAIN, identifier=None, - media_class=MEDIA_CLASS_APP, + media_class=MediaClass.APP, media_content_type="", title="Camera", can_play=False, can_expand=True, - children_media_class=MEDIA_CLASS_VIDEO, + children_media_class=MediaClass.VIDEO, children=children, not_shown=not_shown, ) diff --git a/homeassistant/components/camera/prefs.py b/homeassistant/components/camera/prefs.py index 08c57631a1b..1107da2ba38 100644 --- a/homeassistant/components/camera/prefs.py +++ b/homeassistant/components/camera/prefs.py @@ -1,13 +1,15 @@ """Preference management for camera component.""" from __future__ import annotations -from typing import Final +from typing import Final, Union, cast from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.storage import Store from homeassistant.helpers.typing import UNDEFINED, UndefinedType -from .const import DOMAIN, PREF_PRELOAD_STREAM +from .const import DOMAIN, PREF_ORIENTATION, PREF_PRELOAD_STREAM STORAGE_KEY: Final = DOMAIN STORAGE_VERSION: Final = 1 @@ -16,18 +18,23 @@ STORAGE_VERSION: Final = 1 class CameraEntityPreferences: """Handle preferences for camera entity.""" - def __init__(self, prefs: dict[str, bool]) -> None: + def __init__(self, prefs: dict[str, bool | int]) -> None: """Initialize prefs.""" self._prefs = prefs - def as_dict(self) -> dict[str, bool]: + def as_dict(self) -> dict[str, bool | int]: """Return dictionary version.""" return self._prefs @property def preload_stream(self) -> bool: """Return if stream is loaded on hass start.""" - return self._prefs.get(PREF_PRELOAD_STREAM, False) + return cast(bool, self._prefs.get(PREF_PRELOAD_STREAM, False)) + + @property + def orientation(self) -> int: + """Return the current stream orientation settings.""" + return self._prefs.get(PREF_ORIENTATION, 1) class CameraPreferences: @@ -36,10 +43,13 @@ class CameraPreferences: def __init__(self, hass: HomeAssistant) -> None: """Initialize camera prefs.""" self._hass = hass - self._store = Store[dict[str, dict[str, bool]]]( + # The orientation prefs are stored in in the entity registry options + # The preload_stream prefs are stored in this Store + self._store = Store[dict[str, dict[str, Union[bool, int]]]]( hass, STORAGE_VERSION, STORAGE_KEY ) - self._prefs: dict[str, dict[str, bool]] | None = None + # Local copy of the preload_stream prefs + self._prefs: dict[str, dict[str, bool | int]] | None = None async def async_initialize(self) -> None: """Finish initializing the preferences.""" @@ -53,22 +63,37 @@ class CameraPreferences: entity_id: str, *, preload_stream: bool | UndefinedType = UNDEFINED, + orientation: int | UndefinedType = UNDEFINED, stream_options: dict[str, str] | UndefinedType = UNDEFINED, - ) -> None: - """Update camera preferences.""" - # Prefs already initialized. - assert self._prefs is not None - if not self._prefs.get(entity_id): - self._prefs[entity_id] = {} + ) -> dict[str, bool | int]: + """Update camera preferences. - for key, value in ((PREF_PRELOAD_STREAM, preload_stream),): - if value is not UNDEFINED: - self._prefs[entity_id][key] = value + Returns a dict with the preferences on success. + Raises HomeAssistantError on failure. + """ + if preload_stream is not UNDEFINED: + # Prefs already initialized. + assert self._prefs is not None + if not self._prefs.get(entity_id): + self._prefs[entity_id] = {} + self._prefs[entity_id][PREF_PRELOAD_STREAM] = preload_stream + await self._store.async_save(self._prefs) - await self._store.async_save(self._prefs) + if orientation is not UNDEFINED: + if (registry := er.async_get(self._hass)).async_get(entity_id): + registry.async_update_entity_options( + entity_id, DOMAIN, {PREF_ORIENTATION: orientation} + ) + else: + raise HomeAssistantError( + "Orientation is only supported on entities set up through config flows" + ) + return self.get(entity_id).as_dict() def get(self, entity_id: str) -> CameraEntityPreferences: """Get preferences for an entity.""" # Prefs are already initialized. assert self._prefs is not None - return CameraEntityPreferences(self._prefs.get(entity_id, {})) + reg_entry = er.async_get(self._hass).async_get(entity_id) + er_prefs = reg_entry.options.get(DOMAIN, {}) if reg_entry else {} + return CameraEntityPreferences(self._prefs.get(entity_id, {}) | er_prefs) diff --git a/homeassistant/components/camera/translations/ca.json b/homeassistant/components/camera/translations/ca.json index 0a7b029aced..bf3b1fe0a6c 100644 --- a/homeassistant/components/camera/translations/ca.json +++ b/homeassistant/components/camera/translations/ca.json @@ -3,7 +3,7 @@ "_": { "idle": "Inactiu", "recording": "Enregistrant", - "streaming": "Transmetent v\u00eddeo" + "streaming": "En directe" } }, "title": "C\u00e0mera" diff --git a/homeassistant/components/canary/__init__.py b/homeassistant/components/canary/__init__.py index e6ea20e0768..bc360f99581 100644 --- a/homeassistant/components/canary/__init__.py +++ b/homeassistant/components/canary/__init__.py @@ -9,7 +9,7 @@ from canary.api import Api from requests.exceptions import ConnectTimeout, HTTPError import voluptuous as vol -from homeassistant.components.camera.const import DOMAIN as CAMERA_DOMAIN +from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME, Platform from homeassistant.core import HomeAssistant diff --git a/homeassistant/components/cast/__init__.py b/homeassistant/components/cast/__init__.py index 63f0693b01a..467678ba82b 100644 --- a/homeassistant/components/cast/__init__.py +++ b/homeassistant/components/cast/__init__.py @@ -7,7 +7,7 @@ from typing import Protocol from pychromecast import Chromecast import voluptuous as vol -from homeassistant.components.media_player import BrowseMedia +from homeassistant.components.media_player import BrowseMedia, MediaType from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant @@ -74,7 +74,7 @@ class CastProtocol(Protocol): async def async_browse_media( self, hass: HomeAssistant, - media_content_type: str, + media_content_type: MediaType | str, media_content_id: str, cast_type: str, ) -> BrowseMedia | None: @@ -88,7 +88,7 @@ class CastProtocol(Protocol): hass: HomeAssistant, cast_entity_id: str, chromecast: Chromecast, - media_type: str, + media_type: MediaType | str, media_id: str, ) -> bool: """Play media. diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index 75d3de06856..edd8e0331d9 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -7,7 +7,7 @@ from contextlib import suppress from datetime import datetime import json import logging -from typing import Any +from typing import TYPE_CHECKING, Any import pychromecast from pychromecast.controllers.homeassistant import HomeAssistantController @@ -29,29 +29,21 @@ import yarl from homeassistant.components import media_source, zeroconf from homeassistant.components.media_player import ( + ATTR_MEDIA_EXTRA, BrowseError, BrowseMedia, + MediaClass, MediaPlayerDeviceClass, MediaPlayerEntity, MediaPlayerEntityFeature, + MediaPlayerState, + MediaType, async_process_play_media_url, ) -from homeassistant.components.media_player.const import ( - ATTR_MEDIA_EXTRA, - MEDIA_CLASS_DIRECTORY, - MEDIA_TYPE_MOVIE, - MEDIA_TYPE_MUSIC, - MEDIA_TYPE_TVSHOW, -) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CAST_APP_ID_HOMEASSISTANT_LOVELACE, EVENT_HOMEASSISTANT_STOP, - STATE_BUFFERING, - STATE_IDLE, - STATE_OFF, - STATE_PAUSED, - STATE_PLAYING, ) from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError @@ -83,6 +75,9 @@ from .helpers import ( parse_playlist, ) +if TYPE_CHECKING: + from . import CastProtocol + _LOGGER = logging.getLogger(__name__) APP_IDS_UNRELIABLE_MEDIA_INFO = ("Netflix",) @@ -590,7 +585,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity): return BrowseMedia( title="Cast", - media_class=MEDIA_CLASS_DIRECTORY, + media_class=MediaClass.DIRECTORY, media_content_id="", media_content_type="", can_play=False, @@ -599,7 +594,9 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity): ) async def async_browse_media( - self, media_content_type: str | None = None, media_content_id: str | None = None + self, + media_content_type: MediaType | str | None = None, + media_content_id: str | None = None, ) -> BrowseMedia: """Implement the websocket media browsing helper.""" content_filter = None @@ -619,6 +616,8 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity): if media_content_id is None: return await self._async_root_payload(content_filter) + platform: CastProtocol + assert media_content_type is not None for platform in self.hass.data[CAST_DOMAIN]["cast_platform"].values(): browse_media = await platform.async_browse_media( self.hass, @@ -634,7 +633,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity): ) 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.""" chromecast = self._get_chromecast() @@ -774,27 +773,27 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity): return (media_status, media_status_received) @property - def state(self) -> str | None: + def state(self) -> MediaPlayerState | None: """Return the state of the player.""" # The lovelace app loops media to prevent timing out, don't show that if self.app_id == CAST_APP_ID_HOMEASSISTANT_LOVELACE: - return STATE_PLAYING + return MediaPlayerState.PLAYING if (media_status := self._media_status()[0]) is not None: if media_status.player_state == MEDIA_PLAYER_STATE_PLAYING: - return STATE_PLAYING + return MediaPlayerState.PLAYING if media_status.player_state == MEDIA_PLAYER_STATE_BUFFERING: - return STATE_BUFFERING + return MediaPlayerState.BUFFERING if media_status.player_is_paused: - return STATE_PAUSED + return MediaPlayerState.PAUSED if media_status.player_is_idle: - return STATE_IDLE + return MediaPlayerState.IDLE if self.app_id is not None and self.app_id != pychromecast.IDLE_APP_ID: if self.app_id in APP_IDS_UNRELIABLE_MEDIA_INFO: # Some apps don't report media status, show the player as playing - return STATE_PLAYING - return STATE_IDLE + return MediaPlayerState.PLAYING + return MediaPlayerState.IDLE if self._chromecast is not None and self._chromecast.is_idle: - return STATE_OFF + return MediaPlayerState.OFF return None @property @@ -807,7 +806,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity): return media_status.content_id if media_status else None @property - def media_content_type(self) -> str | None: + def media_content_type(self) -> MediaType | None: """Content type of current playing media.""" # The lovelace app loops media to prevent timing out, don't show that if self.app_id == CAST_APP_ID_HOMEASSISTANT_LOVELACE: @@ -815,11 +814,11 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity): if (media_status := self._media_status()[0]) is None: return None if media_status.media_is_tvshow: - return MEDIA_TYPE_TVSHOW + return MediaType.TVSHOW if media_status.media_is_movie: - return MEDIA_TYPE_MOVIE + return MediaType.MOVIE if media_status.media_is_musictrack: - return MEDIA_TYPE_MUSIC + return MediaType.MUSIC return None @property diff --git a/homeassistant/components/cast/translations/pt.json b/homeassistant/components/cast/translations/pt.json index 34770733822..0238c7cbc94 100644 --- a/homeassistant/components/cast/translations/pt.json +++ b/homeassistant/components/cast/translations/pt.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "Apenas uma \u00fanica configura\u00e7\u00e3o do Google Cast \u00e9 necess\u00e1ria." + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." }, "step": { "config": { diff --git a/homeassistant/components/channels/media_player.py b/homeassistant/components/channels/media_player.py index acb9f7ae680..a834e9010ce 100644 --- a/homeassistant/components/channels/media_player.py +++ b/homeassistant/components/channels/media_player.py @@ -10,22 +10,10 @@ from homeassistant.components.media_player import ( PLATFORM_SCHEMA, MediaPlayerEntity, MediaPlayerEntityFeature, + MediaPlayerState, + MediaType, ) -from homeassistant.components.media_player.const import ( - MEDIA_TYPE_CHANNEL, - MEDIA_TYPE_EPISODE, - MEDIA_TYPE_MOVIE, - MEDIA_TYPE_TVSHOW, -) -from homeassistant.const import ( - ATTR_SECONDS, - CONF_HOST, - CONF_NAME, - CONF_PORT, - STATE_IDLE, - STATE_PAUSED, - STATE_PLAYING, -) +from homeassistant.const import ATTR_SECONDS, CONF_HOST, CONF_NAME, CONF_PORT from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -78,6 +66,7 @@ async def async_setup_platform( class ChannelsPlayer(MediaPlayerEntity): """Representation of a Channels instance.""" + _attr_media_content_type = MediaType.CHANNEL _attr_supported_features = ( MediaPlayerEntityFeature.PLAY | MediaPlayerEntityFeature.PAUSE @@ -156,16 +145,16 @@ class ChannelsPlayer(MediaPlayerEntity): return self._name @property - def state(self): + def state(self) -> MediaPlayerState | None: """Return the state of the player.""" if self.status == "stopped": - return STATE_IDLE + return MediaPlayerState.IDLE if self.status == "paused": - return STATE_PAUSED + return MediaPlayerState.PAUSED if self.status == "playing": - return STATE_PLAYING + return MediaPlayerState.PLAYING return None @@ -190,11 +179,6 @@ class ChannelsPlayer(MediaPlayerEntity): """Content ID of current playing channel.""" return self.channel_number - @property - def media_content_type(self): - """Content type of current playing media.""" - return MEDIA_TYPE_CHANNEL - @property def media_image_url(self): """Image url of current playing media.""" @@ -253,12 +237,14 @@ class ChannelsPlayer(MediaPlayerEntity): self.update_state(response) break - def play_media(self, media_type: str, media_id: str, **kwargs: Any) -> None: + def play_media( + self, media_type: MediaType | str, media_id: str, **kwargs: Any + ) -> None: """Send the play_media command to the player.""" - if media_type == MEDIA_TYPE_CHANNEL: + if media_type == MediaType.CHANNEL: response = self.client.play_channel(media_id) self.update_state(response) - elif media_type in (MEDIA_TYPE_MOVIE, MEDIA_TYPE_EPISODE, MEDIA_TYPE_TVSHOW): + elif media_type in {MediaType.MOVIE, MediaType.EPISODE, MediaType.TVSHOW}: response = self.client.play_recording(media_id) self.update_state(response) diff --git a/homeassistant/components/cisco_ios/device_tracker.py b/homeassistant/components/cisco_ios/device_tracker.py index dc9fe07aa53..b8a4d4cd53d 100644 --- a/homeassistant/components/cisco_ios/device_tracker.py +++ b/homeassistant/components/cisco_ios/device_tracker.py @@ -31,7 +31,7 @@ PLATFORM_SCHEMA = vol.All( ) -def get_scanner(hass: HomeAssistant, config: ConfigType) -> DeviceScanner | None: +def get_scanner(hass: HomeAssistant, config: ConfigType) -> CiscoDeviceScanner | None: """Validate the configuration and return a Cisco scanner.""" scanner = CiscoDeviceScanner(config[DOMAIN]) diff --git a/homeassistant/components/cisco_mobility_express/device_tracker.py b/homeassistant/components/cisco_mobility_express/device_tracker.py index d207922f6a5..a4dff37705b 100644 --- a/homeassistant/components/cisco_mobility_express/device_tracker.py +++ b/homeassistant/components/cisco_mobility_express/device_tracker.py @@ -38,7 +38,7 @@ PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( ) -def get_scanner(hass: HomeAssistant, config: ConfigType) -> DeviceScanner | None: +def get_scanner(hass: HomeAssistant, config: ConfigType) -> CiscoMEDeviceScanner | None: """Validate the configuration and return a Cisco ME scanner.""" config = config[DOMAIN] diff --git a/homeassistant/components/citybikes/sensor.py b/homeassistant/components/citybikes/sensor.py index b9085e24f45..5074378adca 100644 --- a/homeassistant/components/citybikes/sensor.py +++ b/homeassistant/components/citybikes/sensor.py @@ -35,7 +35,8 @@ from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from homeassistant.util import distance, location +from homeassistant.util import location +from homeassistant.util.unit_conversion import DistanceConverter _LOGGER = logging.getLogger(__name__) @@ -170,7 +171,7 @@ async def async_setup_platform( radius = config.get(CONF_RADIUS, 0) name = config[CONF_NAME] if not hass.config.units.is_metric: - radius = distance.convert(radius, LENGTH_FEET, LENGTH_METERS) + radius = DistanceConverter.convert(radius, LENGTH_FEET, LENGTH_METERS) # Create a single instance of CityBikesNetworks. networks = hass.data.setdefault(CITYBIKES_NETWORKS, CityBikesNetworks(hass)) diff --git a/homeassistant/components/clementine/media_player.py b/homeassistant/components/clementine/media_player.py index 06bfb654ea1..770f19e9970 100644 --- a/homeassistant/components/clementine/media_player.py +++ b/homeassistant/components/clementine/media_player.py @@ -11,17 +11,10 @@ from homeassistant.components.media_player import ( PLATFORM_SCHEMA, MediaPlayerEntity, MediaPlayerEntityFeature, + MediaPlayerState, + MediaType, ) -from homeassistant.components.media_player.const import MEDIA_TYPE_MUSIC -from homeassistant.const import ( - CONF_ACCESS_TOKEN, - CONF_HOST, - CONF_NAME, - CONF_PORT, - STATE_OFF, - STATE_PAUSED, - STATE_PLAYING, -) +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST, CONF_NAME, CONF_PORT from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -62,7 +55,7 @@ def setup_platform( class ClementineDevice(MediaPlayerEntity): """Representation of Clementine Player.""" - _attr_media_content_type = MEDIA_TYPE_MUSIC + _attr_media_content_type = MediaType.MUSIC _attr_supported_features = ( MediaPlayerEntityFeature.PAUSE | MediaPlayerEntityFeature.VOLUME_STEP @@ -84,16 +77,16 @@ class ClementineDevice(MediaPlayerEntity): client = self._client if client.state == "Playing": - self._attr_state = STATE_PLAYING + self._attr_state = MediaPlayerState.PLAYING elif client.state == "Paused": - self._attr_state = STATE_PAUSED + self._attr_state = MediaPlayerState.PAUSED elif client.state == "Disconnected": - self._attr_state = STATE_OFF + self._attr_state = MediaPlayerState.OFF else: - self._attr_state = STATE_PAUSED + self._attr_state = MediaPlayerState.PAUSED if client.last_update and (time.time() - client.last_update > 40): - self._attr_state = STATE_OFF + self._attr_state = MediaPlayerState.OFF volume = float(client.volume) if client.volume else 0.0 self._attr_volume_level = volume / 100.0 @@ -112,7 +105,7 @@ class ClementineDevice(MediaPlayerEntity): self._attr_media_image_hash = None except Exception: - self._attr_state = STATE_OFF + self._attr_state = MediaPlayerState.OFF raise def select_source(self, source: str) -> None: @@ -150,19 +143,19 @@ class ClementineDevice(MediaPlayerEntity): def media_play_pause(self) -> None: """Simulate play pause media player.""" - if self.state == STATE_PLAYING: + if self.state == MediaPlayerState.PLAYING: self.media_pause() else: self.media_play() def media_play(self) -> None: """Send play command.""" - self._attr_state = STATE_PLAYING + self._attr_state = MediaPlayerState.PLAYING self._client.play() def media_pause(self) -> None: """Send media pause command to media player.""" - self._attr_state = STATE_PAUSED + self._attr_state = MediaPlayerState.PAUSED self._client.pause() def media_next_track(self) -> None: diff --git a/homeassistant/components/climacell/__init__.py b/homeassistant/components/climacell/__init__.py deleted file mode 100644 index 4f2ad7c5889..00000000000 --- a/homeassistant/components/climacell/__init__.py +++ /dev/null @@ -1,329 +0,0 @@ -"""The ClimaCell integration.""" -from __future__ import annotations - -from datetime import timedelta -import logging -from math import ceil -from typing import Any - -from pyclimacell import ClimaCellV3, ClimaCellV4 -from pyclimacell.const import CURRENT, DAILY, FORECASTS, HOURLY, NOWCAST -from pyclimacell.exceptions import ( - CantConnectException, - InvalidAPIKeyException, - RateLimitedException, - UnknownException, -) - -from homeassistant.components.tomorrowio import DOMAIN as TOMORROW_DOMAIN -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import ( - CONF_API_KEY, - CONF_API_VERSION, - CONF_LATITUDE, - CONF_LONGITUDE, - CONF_NAME, - Platform, -) -from homeassistant.core import HomeAssistant -from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo -from homeassistant.helpers.update_coordinator import ( - CoordinatorEntity, - DataUpdateCoordinator, - UpdateFailed, -) - -from .const import ( - ATTRIBUTION, - CC_V3_ATTR_CLOUD_COVER, - CC_V3_ATTR_CONDITION, - CC_V3_ATTR_HUMIDITY, - CC_V3_ATTR_OZONE, - CC_V3_ATTR_PRECIPITATION, - CC_V3_ATTR_PRECIPITATION_DAILY, - CC_V3_ATTR_PRECIPITATION_PROBABILITY, - CC_V3_ATTR_PRECIPITATION_TYPE, - CC_V3_ATTR_PRESSURE, - CC_V3_ATTR_TEMPERATURE, - CC_V3_ATTR_VISIBILITY, - CC_V3_ATTR_WIND_DIRECTION, - CC_V3_ATTR_WIND_GUST, - CC_V3_ATTR_WIND_SPEED, - CC_V3_SENSOR_TYPES, - CONF_TIMESTEP, - DEFAULT_TIMESTEP, - DOMAIN, - MAX_REQUESTS_PER_DAY, -) - -_LOGGER = logging.getLogger(__name__) - -PLATFORMS = [Platform.SENSOR, Platform.WEATHER] - - -def _set_update_interval(hass: HomeAssistant, current_entry: ConfigEntry) -> timedelta: - """Recalculate update_interval based on existing ClimaCell instances and update them.""" - api_calls = 4 if current_entry.data[CONF_API_VERSION] == 3 else 2 - # We check how many ClimaCell configured instances are using the same API key and - # calculate interval to not exceed allowed numbers of requests. Divide 90% of - # MAX_REQUESTS_PER_DAY by 4 because every update requires four API calls and we want - # a buffer in the number of API calls left at the end of the day. - other_instance_entry_ids = [ - entry.entry_id - for entry in hass.config_entries.async_entries(DOMAIN) - if entry.entry_id != current_entry.entry_id - and entry.data[CONF_API_KEY] == current_entry.data[CONF_API_KEY] - ] - - interval = timedelta( - minutes=( - ceil( - (24 * 60 * (len(other_instance_entry_ids) + 1) * api_calls) - / (MAX_REQUESTS_PER_DAY * 0.9) - ) - ) - ) - - for entry_id in other_instance_entry_ids: - if entry_id in hass.data[DOMAIN]: - hass.data[DOMAIN][entry_id].update_interval = interval - - return interval - - -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Set up ClimaCell API from a config entry.""" - hass.data.setdefault(DOMAIN, {}) - - params: dict[str, Any] = {} - # If config entry options not set up, set them up - if not entry.options: - params["options"] = { - CONF_TIMESTEP: DEFAULT_TIMESTEP, - } - else: - # Use valid timestep if it's invalid - timestep = entry.options[CONF_TIMESTEP] - if timestep not in (1, 5, 15, 30): - if timestep <= 2: - timestep = 1 - elif timestep <= 7: - timestep = 5 - elif timestep <= 20: - timestep = 15 - else: - timestep = 30 - new_options = entry.options.copy() - new_options[CONF_TIMESTEP] = timestep - params["options"] = new_options - # Add API version if not found - if CONF_API_VERSION not in entry.data: - new_data = entry.data.copy() - new_data[CONF_API_VERSION] = 3 - params["data"] = new_data - - if params: - hass.config_entries.async_update_entry(entry, **params) - - hass.async_create_task( - hass.config_entries.flow.async_init( - TOMORROW_DOMAIN, - context={"source": SOURCE_IMPORT, "old_config_entry_id": entry.entry_id}, - data=entry.data, - ) - ) - - # Eventually we will remove the code that sets up the platforms and force users to - # migrate. This will only impact users still on the V3 API because we can't - # automatically migrate them, but for V4 users, we can skip the platform setup. - if entry.data[CONF_API_VERSION] == 4: - return True - - api = ClimaCellV3( - entry.data[CONF_API_KEY], - entry.data.get(CONF_LATITUDE, hass.config.latitude), - entry.data.get(CONF_LONGITUDE, hass.config.longitude), - session=async_get_clientsession(hass), - ) - - coordinator = ClimaCellDataUpdateCoordinator( - hass, - entry, - api, - _set_update_interval(hass, entry), - ) - - await coordinator.async_config_entry_first_refresh() - - 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, config_entry: ConfigEntry) -> bool: - """Unload a config entry.""" - unload_ok = await hass.config_entries.async_unload_platforms( - config_entry, PLATFORMS - ) - - hass.data[DOMAIN].pop(config_entry.entry_id, None) - if not hass.data[DOMAIN]: - hass.data.pop(DOMAIN) - - return unload_ok - - -class ClimaCellDataUpdateCoordinator(DataUpdateCoordinator): - """Define an object to hold ClimaCell data.""" - - def __init__( - self, - hass: HomeAssistant, - config_entry: ConfigEntry, - api: ClimaCellV3 | ClimaCellV4, - update_interval: timedelta, - ) -> None: - """Initialize.""" - - self._config_entry = config_entry - self._api_version = config_entry.data[CONF_API_VERSION] - self._api = api - self.name = config_entry.data[CONF_NAME] - self.data = {CURRENT: {}, FORECASTS: {}} - - super().__init__( - hass, - _LOGGER, - name=config_entry.data[CONF_NAME], - update_interval=update_interval, - ) - - async def _async_update_data(self) -> dict[str, Any]: - """Update data via library.""" - data: dict[str, Any] = {FORECASTS: {}} - try: - data[CURRENT] = await self._api.realtime( - [ - CC_V3_ATTR_TEMPERATURE, - CC_V3_ATTR_HUMIDITY, - CC_V3_ATTR_PRESSURE, - CC_V3_ATTR_WIND_SPEED, - CC_V3_ATTR_WIND_DIRECTION, - CC_V3_ATTR_CONDITION, - CC_V3_ATTR_VISIBILITY, - CC_V3_ATTR_OZONE, - CC_V3_ATTR_WIND_GUST, - CC_V3_ATTR_CLOUD_COVER, - CC_V3_ATTR_PRECIPITATION_TYPE, - *(sensor_type.key for sensor_type in CC_V3_SENSOR_TYPES), - ] - ) - data[FORECASTS][HOURLY] = await self._api.forecast_hourly( - [ - CC_V3_ATTR_TEMPERATURE, - CC_V3_ATTR_WIND_SPEED, - CC_V3_ATTR_WIND_DIRECTION, - CC_V3_ATTR_CONDITION, - CC_V3_ATTR_PRECIPITATION, - CC_V3_ATTR_PRECIPITATION_PROBABILITY, - ], - None, - timedelta(hours=24), - ) - - data[FORECASTS][DAILY] = await self._api.forecast_daily( - [ - CC_V3_ATTR_TEMPERATURE, - CC_V3_ATTR_WIND_SPEED, - CC_V3_ATTR_WIND_DIRECTION, - CC_V3_ATTR_CONDITION, - CC_V3_ATTR_PRECIPITATION_DAILY, - CC_V3_ATTR_PRECIPITATION_PROBABILITY, - ], - None, - timedelta(days=14), - ) - - data[FORECASTS][NOWCAST] = await self._api.forecast_nowcast( - [ - CC_V3_ATTR_TEMPERATURE, - CC_V3_ATTR_WIND_SPEED, - CC_V3_ATTR_WIND_DIRECTION, - CC_V3_ATTR_CONDITION, - CC_V3_ATTR_PRECIPITATION, - ], - None, - timedelta( - minutes=min(300, self._config_entry.options[CONF_TIMESTEP] * 30) - ), - self._config_entry.options[CONF_TIMESTEP], - ) - except ( - CantConnectException, - InvalidAPIKeyException, - RateLimitedException, - UnknownException, - ) as error: - raise UpdateFailed from error - - return data - - -class ClimaCellEntity(CoordinatorEntity[ClimaCellDataUpdateCoordinator]): - """Base ClimaCell Entity.""" - - def __init__( - self, - config_entry: ConfigEntry, - coordinator: ClimaCellDataUpdateCoordinator, - api_version: int, - ) -> None: - """Initialize ClimaCell Entity.""" - super().__init__(coordinator) - self.api_version = api_version - self._config_entry = config_entry - - @staticmethod - def _get_cc_value( - weather_dict: dict[str, Any], key: str - ) -> int | float | str | None: - """ - Return property from weather_dict. - - Used for V3 API. - """ - items = weather_dict.get(key, {}) - # Handle cases where value returned is a list. - # Optimistically find the best value to return. - if isinstance(items, list): - if len(items) == 1: - return items[0].get("value") - return next( - (item.get("value") for item in items if "max" in item), - next( - (item.get("value") for item in items if "min" in item), - items[0].get("value", None), - ), - ) - - return items.get("value") - - @property - def attribution(self): - """Return the attribution.""" - return ATTRIBUTION - - @property - def device_info(self) -> DeviceInfo: - """Return device registry information.""" - return DeviceInfo( - entry_type=DeviceEntryType.SERVICE, - identifiers={(DOMAIN, self._config_entry.data[CONF_API_KEY])}, - manufacturer="ClimaCell", - name="ClimaCell", - sw_version=f"v{self.api_version}", - ) diff --git a/homeassistant/components/climacell/config_flow.py b/homeassistant/components/climacell/config_flow.py deleted file mode 100644 index 07b85e4a4ab..00000000000 --- a/homeassistant/components/climacell/config_flow.py +++ /dev/null @@ -1,52 +0,0 @@ -"""Config flow for ClimaCell integration.""" -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 .const import CONF_TIMESTEP, DEFAULT_TIMESTEP, DOMAIN - - -class ClimaCellOptionsConfigFlow(config_entries.OptionsFlow): - """Handle ClimaCell options.""" - - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: - """Initialize ClimaCell options flow.""" - self._config_entry = config_entry - - async def async_step_init( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: - """Manage the ClimaCell options.""" - if user_input is not None: - return self.async_create_entry(title="", data=user_input) - - options_schema = { - vol.Required( - CONF_TIMESTEP, - default=self._config_entry.options.get(CONF_TIMESTEP, DEFAULT_TIMESTEP), - ): vol.In([1, 5, 15, 30]), - } - - return self.async_show_form( - step_id="init", data_schema=vol.Schema(options_schema) - ) - - -class ClimaCellConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): - """Handle a config flow for ClimaCell Weather API.""" - - VERSION = 1 - - @staticmethod - @callback - def async_get_options_flow( - config_entry: config_entries.ConfigEntry, - ) -> ClimaCellOptionsConfigFlow: - """Get the options flow for this handler.""" - return ClimaCellOptionsConfigFlow(config_entry) diff --git a/homeassistant/components/climacell/const.py b/homeassistant/components/climacell/const.py deleted file mode 100644 index f7ca21259e1..00000000000 --- a/homeassistant/components/climacell/const.py +++ /dev/null @@ -1,225 +0,0 @@ -"""Constants for the ClimaCell integration.""" -from __future__ import annotations - -from collections.abc import Callable -from dataclasses import dataclass -from enum import IntEnum - -from pyclimacell.const import DAILY, HOURLY, NOWCAST, V3PollenIndex - -from homeassistant.components.sensor import SensorDeviceClass, SensorEntityDescription -from homeassistant.components.weather import ( - ATTR_CONDITION_CLEAR_NIGHT, - ATTR_CONDITION_CLOUDY, - ATTR_CONDITION_FOG, - ATTR_CONDITION_HAIL, - ATTR_CONDITION_LIGHTNING, - ATTR_CONDITION_PARTLYCLOUDY, - ATTR_CONDITION_POURING, - ATTR_CONDITION_RAINY, - ATTR_CONDITION_SNOWY, - ATTR_CONDITION_SNOWY_RAINY, - ATTR_CONDITION_SUNNY, - ATTR_CONDITION_WINDY, -) -from homeassistant.const import ( - CONCENTRATION_MICROGRAMS_PER_CUBIC_FOOT, - CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - CONCENTRATION_PARTS_PER_BILLION, - CONCENTRATION_PARTS_PER_MILLION, -) - -CONF_TIMESTEP = "timestep" -FORECAST_TYPES = [DAILY, HOURLY, NOWCAST] - -DEFAULT_NAME = "ClimaCell" -DEFAULT_TIMESTEP = 15 -DEFAULT_FORECAST_TYPE = DAILY -DOMAIN = "climacell" -ATTRIBUTION = "Powered by ClimaCell" - -MAX_REQUESTS_PER_DAY = 100 - -CLEAR_CONDITIONS = {"night": ATTR_CONDITION_CLEAR_NIGHT, "day": ATTR_CONDITION_SUNNY} - -MAX_FORECASTS = { - DAILY: 14, - HOURLY: 24, - NOWCAST: 30, -} - -# Additional attributes -ATTR_WIND_GUST = "wind_gust" -ATTR_CLOUD_COVER = "cloud_cover" -ATTR_PRECIPITATION_TYPE = "precipitation_type" - - -@dataclass -class ClimaCellSensorEntityDescription(SensorEntityDescription): - """Describes a ClimaCell sensor entity.""" - - unit_imperial: str | None = None - unit_metric: str | None = None - metric_conversion: Callable[[float], float] | float = 1.0 - is_metric_check: bool | None = None - device_class: str | None = None - value_map: IntEnum | None = None - - def __post_init__(self) -> None: - """Post initialization.""" - units = (self.unit_imperial, self.unit_metric) - if any(u is not None for u in units) and any(u is None for u in units): - raise RuntimeError( - "`unit_imperial` and `unit_metric` both need to be None or both need " - "to be defined." - ) - - -# V3 constants -CONDITIONS_V3 = { - "breezy": ATTR_CONDITION_WINDY, - "freezing_rain_heavy": ATTR_CONDITION_SNOWY_RAINY, - "freezing_rain": ATTR_CONDITION_SNOWY_RAINY, - "freezing_rain_light": ATTR_CONDITION_SNOWY_RAINY, - "freezing_drizzle": ATTR_CONDITION_SNOWY_RAINY, - "ice_pellets_heavy": ATTR_CONDITION_HAIL, - "ice_pellets": ATTR_CONDITION_HAIL, - "ice_pellets_light": ATTR_CONDITION_HAIL, - "snow_heavy": ATTR_CONDITION_SNOWY, - "snow": ATTR_CONDITION_SNOWY, - "snow_light": ATTR_CONDITION_SNOWY, - "flurries": ATTR_CONDITION_SNOWY, - "tstorm": ATTR_CONDITION_LIGHTNING, - "rain_heavy": ATTR_CONDITION_POURING, - "rain": ATTR_CONDITION_RAINY, - "rain_light": ATTR_CONDITION_RAINY, - "drizzle": ATTR_CONDITION_RAINY, - "fog_light": ATTR_CONDITION_FOG, - "fog": ATTR_CONDITION_FOG, - "cloudy": ATTR_CONDITION_CLOUDY, - "mostly_cloudy": ATTR_CONDITION_CLOUDY, - "partly_cloudy": ATTR_CONDITION_PARTLYCLOUDY, -} - -# Weather attributes -CC_V3_ATTR_TIMESTAMP = "observation_time" -CC_V3_ATTR_TEMPERATURE = "temp" -CC_V3_ATTR_TEMPERATURE_HIGH = "max" -CC_V3_ATTR_TEMPERATURE_LOW = "min" -CC_V3_ATTR_PRESSURE = "baro_pressure" -CC_V3_ATTR_HUMIDITY = "humidity" -CC_V3_ATTR_WIND_SPEED = "wind_speed" -CC_V3_ATTR_WIND_DIRECTION = "wind_direction" -CC_V3_ATTR_OZONE = "o3" -CC_V3_ATTR_CONDITION = "weather_code" -CC_V3_ATTR_VISIBILITY = "visibility" -CC_V3_ATTR_PRECIPITATION = "precipitation" -CC_V3_ATTR_PRECIPITATION_DAILY = "precipitation_accumulation" -CC_V3_ATTR_PRECIPITATION_PROBABILITY = "precipitation_probability" -CC_V3_ATTR_WIND_GUST = "wind_gust" -CC_V3_ATTR_CLOUD_COVER = "cloud_cover" -CC_V3_ATTR_PRECIPITATION_TYPE = "precipitation_type" - -# Sensor attributes -CC_V3_ATTR_PARTICULATE_MATTER_25 = "pm25" -CC_V3_ATTR_PARTICULATE_MATTER_10 = "pm10" -CC_V3_ATTR_NITROGEN_DIOXIDE = "no2" -CC_V3_ATTR_CARBON_MONOXIDE = "co" -CC_V3_ATTR_SULFUR_DIOXIDE = "so2" -CC_V3_ATTR_EPA_AQI = "epa_aqi" -CC_V3_ATTR_EPA_PRIMARY_POLLUTANT = "epa_primary_pollutant" -CC_V3_ATTR_EPA_HEALTH_CONCERN = "epa_health_concern" -CC_V3_ATTR_CHINA_AQI = "china_aqi" -CC_V3_ATTR_CHINA_PRIMARY_POLLUTANT = "china_primary_pollutant" -CC_V3_ATTR_CHINA_HEALTH_CONCERN = "china_health_concern" -CC_V3_ATTR_POLLEN_TREE = "pollen_tree" -CC_V3_ATTR_POLLEN_WEED = "pollen_weed" -CC_V3_ATTR_POLLEN_GRASS = "pollen_grass" -CC_V3_ATTR_FIRE_INDEX = "fire_index" - -CC_V3_SENSOR_TYPES = ( - ClimaCellSensorEntityDescription( - key=CC_V3_ATTR_OZONE, - name="Ozone", - unit_imperial=CONCENTRATION_PARTS_PER_BILLION, - unit_metric=CONCENTRATION_PARTS_PER_BILLION, - ), - ClimaCellSensorEntityDescription( - key=CC_V3_ATTR_PARTICULATE_MATTER_25, - name="Particulate Matter < 2.5 μm", - unit_imperial=CONCENTRATION_MICROGRAMS_PER_CUBIC_FOOT, - unit_metric=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - metric_conversion=3.2808399**3, - is_metric_check=False, - ), - ClimaCellSensorEntityDescription( - key=CC_V3_ATTR_PARTICULATE_MATTER_10, - name="Particulate Matter < 10 μm", - unit_imperial=CONCENTRATION_MICROGRAMS_PER_CUBIC_FOOT, - unit_metric=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - metric_conversion=3.2808399**3, - is_metric_check=False, - ), - ClimaCellSensorEntityDescription( - key=CC_V3_ATTR_NITROGEN_DIOXIDE, - name="Nitrogen Dioxide", - unit_imperial=CONCENTRATION_PARTS_PER_BILLION, - unit_metric=CONCENTRATION_PARTS_PER_BILLION, - ), - ClimaCellSensorEntityDescription( - key=CC_V3_ATTR_CARBON_MONOXIDE, - name="Carbon Monoxide", - unit_imperial=CONCENTRATION_PARTS_PER_MILLION, - unit_metric=CONCENTRATION_PARTS_PER_MILLION, - device_class=SensorDeviceClass.CO, - ), - ClimaCellSensorEntityDescription( - key=CC_V3_ATTR_SULFUR_DIOXIDE, - name="Sulfur Dioxide", - unit_imperial=CONCENTRATION_PARTS_PER_BILLION, - unit_metric=CONCENTRATION_PARTS_PER_BILLION, - ), - ClimaCellSensorEntityDescription( - key=CC_V3_ATTR_EPA_AQI, - name="US EPA Air Quality Index", - ), - ClimaCellSensorEntityDescription( - key=CC_V3_ATTR_EPA_PRIMARY_POLLUTANT, - name="US EPA Primary Pollutant", - ), - ClimaCellSensorEntityDescription( - key=CC_V3_ATTR_EPA_HEALTH_CONCERN, - name="US EPA Health Concern", - ), - ClimaCellSensorEntityDescription( - key=CC_V3_ATTR_CHINA_AQI, - name="China MEP Air Quality Index", - ), - ClimaCellSensorEntityDescription( - key=CC_V3_ATTR_CHINA_PRIMARY_POLLUTANT, - name="China MEP Primary Pollutant", - ), - ClimaCellSensorEntityDescription( - key=CC_V3_ATTR_CHINA_HEALTH_CONCERN, - name="China MEP Health Concern", - ), - ClimaCellSensorEntityDescription( - key=CC_V3_ATTR_POLLEN_TREE, - name="Tree Pollen Index", - value_map=V3PollenIndex, - ), - ClimaCellSensorEntityDescription( - key=CC_V3_ATTR_POLLEN_WEED, - name="Weed Pollen Index", - value_map=V3PollenIndex, - ), - ClimaCellSensorEntityDescription( - key=CC_V3_ATTR_POLLEN_GRASS, - name="Grass Pollen Index", - value_map=V3PollenIndex, - ), - ClimaCellSensorEntityDescription( - key=CC_V3_ATTR_FIRE_INDEX, - name="Fire Index", - ), -) diff --git a/homeassistant/components/climacell/manifest.json b/homeassistant/components/climacell/manifest.json deleted file mode 100644 index f0eee5ef0da..00000000000 --- a/homeassistant/components/climacell/manifest.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "domain": "climacell", - "name": "ClimaCell", - "config_flow": false, - "documentation": "https://www.home-assistant.io/integrations/climacell", - "requirements": ["pyclimacell==0.18.2"], - "after_dependencies": ["tomorrowio"], - "codeowners": ["@raman325"], - "iot_class": "cloud_polling", - "loggers": ["pyclimacell"] -} diff --git a/homeassistant/components/climacell/sensor.py b/homeassistant/components/climacell/sensor.py deleted file mode 100644 index 4eb9dddb9c3..00000000000 --- a/homeassistant/components/climacell/sensor.py +++ /dev/null @@ -1,88 +0,0 @@ -"""Sensor component that handles additional ClimaCell data for your location.""" -from __future__ import annotations - -from pyclimacell.const import CURRENT - -from homeassistant.components.sensor import SensorEntity -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_ATTRIBUTION, CONF_API_VERSION, CONF_NAME -from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.util import slugify - -from . import ClimaCellDataUpdateCoordinator, ClimaCellEntity -from .const import CC_V3_SENSOR_TYPES, DOMAIN, ClimaCellSensorEntityDescription - - -async def async_setup_entry( - hass: HomeAssistant, - config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up a config entry.""" - coordinator = hass.data[DOMAIN][config_entry.entry_id] - api_version = config_entry.data[CONF_API_VERSION] - entities = [ - ClimaCellV3SensorEntity( - hass, config_entry, coordinator, api_version, description - ) - for description in CC_V3_SENSOR_TYPES - ] - async_add_entities(entities) - - -class ClimaCellV3SensorEntity(ClimaCellEntity, SensorEntity): - """Sensor entity that talks to ClimaCell v3 API to retrieve non-weather data.""" - - entity_description: ClimaCellSensorEntityDescription - - def __init__( - self, - hass: HomeAssistant, - config_entry: ConfigEntry, - coordinator: ClimaCellDataUpdateCoordinator, - api_version: int, - description: ClimaCellSensorEntityDescription, - ) -> None: - """Initialize ClimaCell Sensor Entity.""" - super().__init__(config_entry, coordinator, api_version) - self.entity_description = description - self._attr_entity_registry_enabled_default = False - self._attr_name = f"{self._config_entry.data[CONF_NAME]} - {description.name}" - self._attr_unique_id = ( - f"{self._config_entry.unique_id}_{slugify(description.name)}" - ) - self._attr_extra_state_attributes = {ATTR_ATTRIBUTION: self.attribution} - self._attr_native_unit_of_measurement = ( - description.unit_metric - if hass.config.units.is_metric - else description.unit_imperial - ) - - @property - def native_value(self) -> str | int | float | None: - """Return the state.""" - state = self._get_cc_value( - self.coordinator.data[CURRENT], self.entity_description.key - ) - if ( - state is not None - and not isinstance(state, str) - and self.entity_description.unit_imperial is not None - and self.entity_description.metric_conversion != 1.0 - and self.entity_description.is_metric_check is not None - and self.hass.config.units.is_metric - == self.entity_description.is_metric_check - ): - conversion = self.entity_description.metric_conversion - # When conversion is a callable, we assume it's a single input function - if callable(conversion): - return round(conversion(state), 4) - - return round(state * conversion, 4) - - if self.entity_description.value_map is not None and state is not None: - # mypy bug: "Literal[IntEnum.value]" not callable - return self.entity_description.value_map(state).name.lower() # type: ignore[misc] - - return state diff --git a/homeassistant/components/climacell/strings.json b/homeassistant/components/climacell/strings.json deleted file mode 100644 index 25ddee09dd0..00000000000 --- a/homeassistant/components/climacell/strings.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "options": { - "step": { - "init": { - "title": "Update ClimaCell Options", - "description": "If you choose to enable the `nowcast` forecast entity, you can configure the number of minutes between each forecast. The number of forecasts provided depends on the number of minutes chosen between forecasts.", - "data": { - "timestep": "Min. Between NowCast Forecasts" - } - } - } - } -} diff --git a/homeassistant/components/climacell/strings.sensor.json b/homeassistant/components/climacell/strings.sensor.json deleted file mode 100644 index 1864a034043..00000000000 --- a/homeassistant/components/climacell/strings.sensor.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "state": { - "climacell__pollen_index": { - "none": "None", - "very_low": "Very Low", - "low": "Low", - "medium": "Medium", - "high": "High", - "very_high": "Very High" - }, - "climacell__health_concern": { - "good": "Good", - "moderate": "Moderate", - "unhealthy_for_sensitive_groups": "Unhealthy for Sensitive Groups", - "unhealthy": "Unhealthy", - "very_unhealthy": "Very Unhealthy", - "hazardous": "Hazardous" - }, - "climacell__precipitation_type": { - "none": "None", - "rain": "Rain", - "snow": "Snow", - "freezing_rain": "Freezing Rain", - "ice_pellets": "Ice Pellets" - } - } -} diff --git a/homeassistant/components/climacell/translations/ja.json b/homeassistant/components/climacell/translations/ja.json index 5c78820c853..e2742d11435 100644 --- a/homeassistant/components/climacell/translations/ja.json +++ b/homeassistant/components/climacell/translations/ja.json @@ -3,10 +3,10 @@ "step": { "init": { "data": { - "timestep": "\u6700\u5c0f: NowCast Forecasts\u306e\u9593" + "timestep": "\u6700\u5c0f\u3002 NowCast \u4e88\u6e2c\u306e\u9593" }, "description": "`nowcast` forecast(\u4e88\u6e2c) \u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u3092\u6709\u52b9\u306b\u3059\u308b\u3053\u3068\u3092\u9078\u629e\u3057\u305f\u5834\u5408\u3001\u5404\u4e88\u6e2c\u9593\u306e\u5206\u6570\u3092\u8a2d\u5b9a\u3059\u308b\u3053\u3068\u304c\u3067\u304d\u307e\u3059\u3002\u63d0\u4f9b\u3055\u308c\u308bforecast(\u4e88\u6e2c)\u306e\u6570\u306f\u3001forecast(\u4e88\u6e2c)\u306e\u9593\u306b\u9078\u629e\u3057\u305f\u5206\u6570\u306b\u4f9d\u5b58\u3057\u307e\u3059\u3002", - "title": "[%key:component::climacell::title%]\u30aa\u30d7\u30b7\u30e7\u30f3\u3092\u66f4\u65b0\u3057\u307e\u3059" + "title": "ClimaCell \u30aa\u30d7\u30b7\u30e7\u30f3\u306e\u66f4\u65b0" } } } diff --git a/homeassistant/components/climacell/weather.py b/homeassistant/components/climacell/weather.py deleted file mode 100644 index 73ff6361041..00000000000 --- a/homeassistant/components/climacell/weather.py +++ /dev/null @@ -1,361 +0,0 @@ -"""Weather component that handles meteorological data for your location.""" -from __future__ import annotations - -from abc import abstractmethod -from collections.abc import Mapping -from datetime import datetime -from typing import Any, cast - -from pyclimacell.const import CURRENT, DAILY, FORECASTS, HOURLY, NOWCAST - -from homeassistant.components.weather import ( - ATTR_FORECAST_CONDITION, - ATTR_FORECAST_NATIVE_PRECIPITATION, - ATTR_FORECAST_NATIVE_TEMP, - ATTR_FORECAST_NATIVE_TEMP_LOW, - ATTR_FORECAST_NATIVE_WIND_SPEED, - ATTR_FORECAST_PRECIPITATION_PROBABILITY, - ATTR_FORECAST_TIME, - ATTR_FORECAST_WIND_BEARING, - WeatherEntity, -) -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - CONF_API_VERSION, - CONF_NAME, - LENGTH_INCHES, - LENGTH_MILES, - PRESSURE_INHG, - SPEED_MILES_PER_HOUR, - TEMP_FAHRENHEIT, -) -from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.sun import is_up -from homeassistant.util import dt as dt_util -from homeassistant.util.speed import convert as speed_convert - -from . import ClimaCellDataUpdateCoordinator, ClimaCellEntity -from .const import ( - ATTR_CLOUD_COVER, - ATTR_PRECIPITATION_TYPE, - ATTR_WIND_GUST, - CC_V3_ATTR_CLOUD_COVER, - CC_V3_ATTR_CONDITION, - CC_V3_ATTR_HUMIDITY, - CC_V3_ATTR_OZONE, - CC_V3_ATTR_PRECIPITATION, - CC_V3_ATTR_PRECIPITATION_DAILY, - CC_V3_ATTR_PRECIPITATION_PROBABILITY, - CC_V3_ATTR_PRECIPITATION_TYPE, - CC_V3_ATTR_PRESSURE, - CC_V3_ATTR_TEMPERATURE, - CC_V3_ATTR_TEMPERATURE_HIGH, - CC_V3_ATTR_TEMPERATURE_LOW, - CC_V3_ATTR_TIMESTAMP, - CC_V3_ATTR_VISIBILITY, - CC_V3_ATTR_WIND_DIRECTION, - CC_V3_ATTR_WIND_GUST, - CC_V3_ATTR_WIND_SPEED, - CLEAR_CONDITIONS, - CONDITIONS_V3, - CONF_TIMESTEP, - DEFAULT_FORECAST_TYPE, - DOMAIN, -) - - -async def async_setup_entry( - hass: HomeAssistant, - config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up a config entry.""" - coordinator = hass.data[DOMAIN][config_entry.entry_id] - api_version = config_entry.data[CONF_API_VERSION] - entities = [ - ClimaCellV3WeatherEntity(config_entry, coordinator, api_version, forecast_type) - for forecast_type in (DAILY, HOURLY, NOWCAST) - ] - async_add_entities(entities) - - -class BaseClimaCellWeatherEntity(ClimaCellEntity, WeatherEntity): - """Base ClimaCell weather entity.""" - - _attr_native_precipitation_unit = LENGTH_INCHES - _attr_native_pressure_unit = PRESSURE_INHG - _attr_native_temperature_unit = TEMP_FAHRENHEIT - _attr_native_visibility_unit = LENGTH_MILES - _attr_native_wind_speed_unit = SPEED_MILES_PER_HOUR - - def __init__( - self, - config_entry: ConfigEntry, - coordinator: ClimaCellDataUpdateCoordinator, - api_version: int, - forecast_type: str, - ) -> None: - """Initialize ClimaCell Weather Entity.""" - super().__init__(config_entry, coordinator, api_version) - self.forecast_type = forecast_type - self._attr_entity_registry_enabled_default = ( - forecast_type == DEFAULT_FORECAST_TYPE - ) - self._attr_name = f"{config_entry.data[CONF_NAME]} - {forecast_type.title()}" - self._attr_unique_id = f"{config_entry.unique_id}_{forecast_type}" - - @staticmethod - @abstractmethod - def _translate_condition( - condition: str | int | None, sun_is_up: bool = True - ) -> str | None: - """Translate ClimaCell condition into an HA condition.""" - - def _forecast_dict( - self, - forecast_dt: datetime, - use_datetime: bool, - condition: int | str, - precipitation: float | None, - precipitation_probability: float | None, - temp: float | None, - temp_low: float | None, - wind_direction: float | None, - wind_speed: float | None, - ) -> dict[str, Any]: - """Return formatted Forecast dict from ClimaCell forecast data.""" - if use_datetime: - translated_condition = self._translate_condition( - condition, is_up(self.hass, forecast_dt) - ) - else: - translated_condition = self._translate_condition(condition, True) - - data = { - ATTR_FORECAST_TIME: forecast_dt.isoformat(), - ATTR_FORECAST_CONDITION: translated_condition, - ATTR_FORECAST_NATIVE_PRECIPITATION: precipitation, - ATTR_FORECAST_PRECIPITATION_PROBABILITY: precipitation_probability, - ATTR_FORECAST_NATIVE_TEMP: temp, - ATTR_FORECAST_NATIVE_TEMP_LOW: temp_low, - ATTR_FORECAST_WIND_BEARING: wind_direction, - ATTR_FORECAST_NATIVE_WIND_SPEED: wind_speed, - } - - return {k: v for k, v in data.items() if v is not None} - - @property - def extra_state_attributes(self) -> Mapping[str, Any] | None: - """Return additional state attributes.""" - cloud_cover = self.cloud_cover - attrs = { - ATTR_CLOUD_COVER: cloud_cover, - ATTR_PRECIPITATION_TYPE: self.precipitation_type, - } - if (wind_gust := self.wind_gust) is not None: - attrs[ATTR_WIND_GUST] = round( - speed_convert(wind_gust, SPEED_MILES_PER_HOUR, self._wind_speed_unit), 4 - ) - return attrs - - @property - @abstractmethod - def cloud_cover(self): - """Return cloud cover.""" - - @property - @abstractmethod - def wind_gust(self): - """Return wind gust speed.""" - - @property - @abstractmethod - def precipitation_type(self): - """Return precipitation type.""" - - @property - @abstractmethod - def _pressure(self): - """Return the raw pressure.""" - - @property - def native_pressure(self): - """Return the pressure.""" - return self._pressure - - @property - @abstractmethod - def _wind_speed(self): - """Return the raw wind speed.""" - - @property - def native_wind_speed(self): - """Return the wind speed.""" - return self._wind_speed - - @property - @abstractmethod - def _visibility(self): - """Return the raw visibility.""" - - @property - def native_visibility(self): - """Return the visibility.""" - return self._visibility - - -class ClimaCellV3WeatherEntity(BaseClimaCellWeatherEntity): - """Entity that talks to ClimaCell v3 API to retrieve weather data.""" - - @staticmethod - def _translate_condition( - condition: int | str | None, sun_is_up: bool = True - ) -> str | None: - """Translate ClimaCell condition into an HA condition.""" - if not condition: - return None - condition = cast(str, condition) - if "clear" in condition.lower(): - if sun_is_up: - return CLEAR_CONDITIONS["day"] - return CLEAR_CONDITIONS["night"] - return CONDITIONS_V3[condition] - - @property - def native_temperature(self): - """Return the platform temperature.""" - return self._get_cc_value( - self.coordinator.data[CURRENT], CC_V3_ATTR_TEMPERATURE - ) - - @property - def _pressure(self): - """Return the raw pressure.""" - return self._get_cc_value(self.coordinator.data[CURRENT], CC_V3_ATTR_PRESSURE) - - @property - def humidity(self): - """Return the humidity.""" - return self._get_cc_value(self.coordinator.data[CURRENT], CC_V3_ATTR_HUMIDITY) - - @property - def wind_gust(self): - """Return the wind gust speed.""" - return self._get_cc_value(self.coordinator.data[CURRENT], CC_V3_ATTR_WIND_GUST) - - @property - def cloud_cover(self): - """Return the cloud cover.""" - return self._get_cc_value( - self.coordinator.data[CURRENT], CC_V3_ATTR_CLOUD_COVER - ) - - @property - def precipitation_type(self): - """Return precipitation type.""" - return self._get_cc_value( - self.coordinator.data[CURRENT], CC_V3_ATTR_PRECIPITATION_TYPE - ) - - @property - def _wind_speed(self): - """Return the raw wind speed.""" - return self._get_cc_value(self.coordinator.data[CURRENT], CC_V3_ATTR_WIND_SPEED) - - @property - def wind_bearing(self): - """Return the wind bearing.""" - return self._get_cc_value( - self.coordinator.data[CURRENT], CC_V3_ATTR_WIND_DIRECTION - ) - - @property - def ozone(self): - """Return the O3 (ozone) level.""" - return self._get_cc_value(self.coordinator.data[CURRENT], CC_V3_ATTR_OZONE) - - @property - def condition(self): - """Return the condition.""" - return self._translate_condition( - self._get_cc_value(self.coordinator.data[CURRENT], CC_V3_ATTR_CONDITION), - is_up(self.hass), - ) - - @property - def _visibility(self): - """Return the raw visibility.""" - return self._get_cc_value(self.coordinator.data[CURRENT], CC_V3_ATTR_VISIBILITY) - - @property - def forecast(self): - """Return the forecast.""" - # Check if forecasts are available - raw_forecasts = self.coordinator.data.get(FORECASTS, {}).get(self.forecast_type) - if not raw_forecasts: - return None - - forecasts = [] - - # Set default values (in cases where keys don't exist), None will be - # returned. Override properties per forecast type as needed - for forecast in raw_forecasts: - forecast_dt = dt_util.parse_datetime( - self._get_cc_value(forecast, CC_V3_ATTR_TIMESTAMP) - ) - use_datetime = True - condition = self._get_cc_value(forecast, CC_V3_ATTR_CONDITION) - precipitation = self._get_cc_value(forecast, CC_V3_ATTR_PRECIPITATION) - precipitation_probability = self._get_cc_value( - forecast, CC_V3_ATTR_PRECIPITATION_PROBABILITY - ) - temp = self._get_cc_value(forecast, CC_V3_ATTR_TEMPERATURE) - temp_low = None - wind_direction = self._get_cc_value(forecast, CC_V3_ATTR_WIND_DIRECTION) - wind_speed = self._get_cc_value(forecast, CC_V3_ATTR_WIND_SPEED) - - if self.forecast_type == DAILY: - use_datetime = False - forecast_dt = dt_util.start_of_local_day(forecast_dt) - precipitation = self._get_cc_value( - forecast, CC_V3_ATTR_PRECIPITATION_DAILY - ) - temp = next( - ( - self._get_cc_value(item, CC_V3_ATTR_TEMPERATURE_HIGH) - for item in forecast[CC_V3_ATTR_TEMPERATURE] - if "max" in item - ), - temp, - ) - temp_low = next( - ( - self._get_cc_value(item, CC_V3_ATTR_TEMPERATURE_LOW) - for item in forecast[CC_V3_ATTR_TEMPERATURE] - if "min" in item - ), - temp_low, - ) - elif self.forecast_type == NOWCAST and precipitation: - # Precipitation is forecasted in CONF_TIMESTEP increments but in a - # per hour rate, so value needs to be converted to an amount. - precipitation = ( - precipitation / 60 * self._config_entry.options[CONF_TIMESTEP] - ) - - forecasts.append( - self._forecast_dict( - forecast_dt, - use_datetime, - condition, - precipitation, - precipitation_probability, - temp, - temp_low, - wind_direction, - wind_speed, - ) - ) - - return forecasts diff --git a/homeassistant/components/climate/__init__.py b/homeassistant/components/climate/__init__.py index 818caaaa78d..67348d38625 100644 --- a/homeassistant/components/climate/__init__.py +++ b/homeassistant/components/climate/__init__.py @@ -31,7 +31,7 @@ from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.temperature import display_temp as show_temp from homeassistant.helpers.typing import ConfigType -from homeassistant.util.temperature import convert as convert_temperature +from homeassistant.util.unit_conversion import TemperatureConverter from .const import ( # noqa: F401 ATTR_AUX_HEAT, @@ -55,11 +55,29 @@ from .const import ( # noqa: F401 ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_STEP, DOMAIN, + FAN_AUTO, + FAN_DIFFUSE, + FAN_FOCUS, + FAN_HIGH, + FAN_LOW, + FAN_MEDIUM, + FAN_MIDDLE, + FAN_OFF, + FAN_ON, + FAN_TOP, HVAC_MODE_COOL, HVAC_MODE_HEAT, HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF, HVAC_MODES, + PRESET_ACTIVITY, + PRESET_AWAY, + PRESET_BOOST, + PRESET_COMFORT, + PRESET_ECO, + PRESET_HOME, + PRESET_NONE, + PRESET_SLEEP, SERVICE_SET_AUX_HEAT, SERVICE_SET_FAN_MODE, SERVICE_SET_HUMIDITY, @@ -74,6 +92,11 @@ from .const import ( # noqa: F401 SUPPORT_TARGET_HUMIDITY, SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE_RANGE, + SWING_BOTH, + SWING_HORIZONTAL, + SWING_OFF, + SWING_ON, + SWING_VERTICAL, ClimateEntityFeature, HVACAction, HVACMode, @@ -106,10 +129,12 @@ SET_TEMPERATURE_SCHEMA = vol.All( ), ) +# mypy: disallow-any-generics + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up climate entities.""" - component = hass.data[DOMAIN] = EntityComponent( + component = hass.data[DOMAIN] = EntityComponent[ClimateEntity]( _LOGGER, DOMAIN, hass, SCAN_INTERVAL ) await component.async_setup(config) @@ -166,13 +191,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry.""" - component: EntityComponent = hass.data[DOMAIN] + component: EntityComponent[ClimateEntity] = hass.data[DOMAIN] return await component.async_setup_entry(entry) async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - component: EntityComponent = hass.data[DOMAIN] + component: EntityComponent[ClimateEntity] = hass.data[DOMAIN] return await component.async_unload_entry(entry) @@ -535,7 +560,7 @@ class ClimateEntity(Entity): def min_temp(self) -> float: """Return the minimum temperature.""" if not hasattr(self, "_attr_min_temp"): - return convert_temperature( + return TemperatureConverter.convert( DEFAULT_MIN_TEMP, TEMP_CELSIUS, self.temperature_unit ) return self._attr_min_temp @@ -544,7 +569,7 @@ class ClimateEntity(Entity): def max_temp(self) -> float: """Return the maximum temperature.""" if not hasattr(self, "_attr_max_temp"): - return convert_temperature( + return TemperatureConverter.convert( DEFAULT_MAX_TEMP, TEMP_CELSIUS, self.temperature_unit ) return self._attr_max_temp @@ -579,7 +604,7 @@ async def async_service_temperature_set( for value, temp in service_call.data.items(): if value in CONVERTIBLE_ATTRIBUTE: - kwargs[value] = convert_temperature( + kwargs[value] = TemperatureConverter.convert( temp, hass.config.units.temperature_unit, entity.temperature_unit ) else: diff --git a/homeassistant/components/climate/manifest.json b/homeassistant/components/climate/manifest.json index 8b54d3a91ad..7c23705181a 100644 --- a/homeassistant/components/climate/manifest.json +++ b/homeassistant/components/climate/manifest.json @@ -3,5 +3,6 @@ "name": "Climate", "documentation": "https://www.home-assistant.io/integrations/climate", "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "entity" } diff --git a/homeassistant/components/climate/translations/cs.json b/homeassistant/components/climate/translations/cs.json index 3740a7b423e..276ef1a7d70 100644 --- a/homeassistant/components/climate/translations/cs.json +++ b/homeassistant/components/climate/translations/cs.json @@ -20,8 +20,8 @@ "cool": "Chlazen\u00ed", "dry": "Vysou\u0161en\u00ed", "fan_only": "Pouze ventil\u00e1tor", - "heat": "Topen\u00ed", - "heat_cool": "Topen\u00ed/Chlazen\u00ed", + "heat": "Vyt\u00e1p\u011bn\u00ed", + "heat_cool": "Vyt\u00e1p\u011bn\u00ed/Chlazen\u00ed", "off": "Vypnuto" } }, diff --git a/homeassistant/components/cloud/__init__.py b/homeassistant/components/cloud/__init__.py index aa31f796491..6a948c0ad15 100644 --- a/homeassistant/components/cloud/__init__.py +++ b/homeassistant/components/cloud/__init__.py @@ -8,8 +8,7 @@ from enum import Enum from hass_nabucasa import Cloud import voluptuous as vol -from homeassistant.components.alexa import const as alexa_const -from homeassistant.components.google_assistant import const as ga_c +from homeassistant.components import alexa, google_assistant from homeassistant.const import ( CONF_DESCRIPTION, CONF_MODE, @@ -68,7 +67,7 @@ SIGNAL_CLOUD_CONNECTION_STATE = "CLOUD_CONNECTION_STATE" ALEXA_ENTITY_SCHEMA = vol.Schema( { vol.Optional(CONF_DESCRIPTION): cv.string, - vol.Optional(alexa_const.CONF_DISPLAY_CATEGORIES): cv.string, + vol.Optional(alexa.CONF_DISPLAY_CATEGORIES): cv.string, vol.Optional(CONF_NAME): cv.string, } ) @@ -77,7 +76,7 @@ GOOGLE_ENTITY_SCHEMA = vol.Schema( { vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_ALIASES): vol.All(cv.ensure_list, [cv.string]), - vol.Optional(ga_c.CONF_ROOM_HINT): cv.string, + vol.Optional(google_assistant.CONF_ROOM_HINT): cv.string, } ) diff --git a/homeassistant/components/cloud/client.py b/homeassistant/components/cloud/client.py index 6011e9bf551..04b9a9aab97 100644 --- a/homeassistant/components/cloud/client.py +++ b/homeassistant/components/cloud/client.py @@ -10,12 +10,12 @@ from typing import Any import aiohttp from hass_nabucasa.client import CloudClient as Interface -from homeassistant.components import persistent_notification, webhook +from homeassistant.components import google_assistant, persistent_notification, webhook from homeassistant.components.alexa import ( errors as alexa_errors, smart_home as alexa_smart_home, ) -from homeassistant.components.google_assistant import const as gc, smart_home as ga +from homeassistant.components.google_assistant import smart_home as ga from homeassistant.core import Context, HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_call_later @@ -216,7 +216,7 @@ class CloudClient(Interface): return ga.api_disabled_response(payload, gconf.agent_user_id) return await ga.async_handle_message( - self._hass, gconf, gconf.cloud_user, payload, gc.SOURCE_CLOUD + self._hass, gconf, gconf.cloud_user, payload, google_assistant.SOURCE_CLOUD ) async def async_webhook_message(self, payload: dict[Any, Any]) -> dict[Any, Any]: diff --git a/homeassistant/components/cloud/google_config.py b/homeassistant/components/cloud/google_config.py index 9bb2e405dca..42570dfff6e 100644 --- a/homeassistant/components/cloud/google_config.py +++ b/homeassistant/components/cloud/google_config.py @@ -6,7 +6,7 @@ import logging from hass_nabucasa import Cloud, cloud_api from hass_nabucasa.google_report_state import ErrorResponse -from homeassistant.components.google_assistant.const import DOMAIN as GOOGLE_DOMAIN +from homeassistant.components.google_assistant import DOMAIN as GOOGLE_DOMAIN from homeassistant.components.google_assistant.helpers import AbstractConfig from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES from homeassistant.core import CoreState, Event, callback, split_entity_id diff --git a/homeassistant/components/cloud/http_api.py b/homeassistant/components/cloud/http_api.py index 8e5c214b388..ebeb79dcd2a 100644 --- a/homeassistant/components/cloud/http_api.py +++ b/homeassistant/components/cloud/http_api.py @@ -21,7 +21,6 @@ from homeassistant.components.alexa import ( from homeassistant.components.google_assistant import helpers as google_helpers from homeassistant.components.http import HomeAssistantView from homeassistant.components.http.data_validator import RequestDataValidator -from homeassistant.components.websocket_api import const as ws_const from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.util.location import async_detect_location_info @@ -616,7 +615,9 @@ async def alexa_sync(hass, connection, msg): if success: connection.send_result(msg["id"]) else: - connection.send_error(msg["id"], ws_const.ERR_UNKNOWN_ERROR, "Unknown error") + connection.send_error( + msg["id"], websocket_api.ERR_UNKNOWN_ERROR, "Unknown error" + ) @websocket_api.websocket_command({"type": "cloud/thingtalk/convert", "query": str}) @@ -631,7 +632,7 @@ async def thingtalk_convert(hass, connection, msg): msg["id"], await thingtalk.async_convert(cloud, msg["query"]) ) except thingtalk.ThingTalkConversionError as err: - connection.send_error(msg["id"], ws_const.ERR_UNKNOWN_ERROR, str(err)) + connection.send_error(msg["id"], websocket_api.ERR_UNKNOWN_ERROR, str(err)) @websocket_api.websocket_command({"type": "cloud/tts/info"}) diff --git a/homeassistant/components/cloud/manifest.json b/homeassistant/components/cloud/manifest.json index 02ffa0a4775..3a6d942f5ea 100644 --- a/homeassistant/components/cloud/manifest.json +++ b/homeassistant/components/cloud/manifest.json @@ -2,7 +2,7 @@ "domain": "cloud", "name": "Home Assistant Cloud", "documentation": "https://www.home-assistant.io/integrations/cloud", - "requirements": ["hass-nabucasa==0.55.0"], + "requirements": ["hass-nabucasa==0.56.0"], "dependencies": ["http", "webhook"], "after_dependencies": ["google_assistant", "alexa"], "codeowners": ["@home-assistant/cloud"], diff --git a/homeassistant/components/cloud/stt.py b/homeassistant/components/cloud/stt.py index 80578a8d721..b1798b2f3be 100644 --- a/homeassistant/components/cloud/stt.py +++ b/homeassistant/components/cloud/stt.py @@ -5,13 +5,15 @@ from aiohttp import StreamReader from hass_nabucasa import Cloud from hass_nabucasa.voice import VoiceError -from homeassistant.components.stt import Provider, SpeechMetadata, SpeechResult -from homeassistant.components.stt.const import ( +from homeassistant.components.stt import ( AudioBitRates, AudioChannels, AudioCodecs, AudioFormats, AudioSampleRates, + Provider, + SpeechMetadata, + SpeechResult, SpeechResultState, ) diff --git a/homeassistant/components/cloudflare/translations/es.json b/homeassistant/components/cloudflare/translations/es.json index d47711bf0a5..a711ccfd819 100644 --- a/homeassistant/components/cloudflare/translations/es.json +++ b/homeassistant/components/cloudflare/translations/es.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente", "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n.", "unknown": "Error inesperado" }, @@ -15,7 +15,7 @@ "reauth_confirm": { "data": { "api_token": "Token API", - "description": "Vuelve a autenticarte con tu cuenta de Cloudflare." + "description": "Vuelve a autenticarte con tu cuenta Cloudflare." } }, "records": { diff --git a/homeassistant/components/cloudflare/translations/pt.json b/homeassistant/components/cloudflare/translations/pt.json index 650823693d6..dd836339b2b 100644 --- a/homeassistant/components/cloudflare/translations/pt.json +++ b/homeassistant/components/cloudflare/translations/pt.json @@ -10,6 +10,11 @@ "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" }, "step": { + "reauth_confirm": { + "data": { + "api_token": "API Token" + } + }, "user": { "data": { "api_token": "API Token" diff --git a/homeassistant/components/cmus/media_player.py b/homeassistant/components/cmus/media_player.py index 09fec24b543..65bfef3a0cb 100644 --- a/homeassistant/components/cmus/media_player.py +++ b/homeassistant/components/cmus/media_player.py @@ -11,20 +11,10 @@ from homeassistant.components.media_player import ( PLATFORM_SCHEMA, MediaPlayerEntity, MediaPlayerEntityFeature, + MediaPlayerState, + MediaType, ) -from homeassistant.components.media_player.const import ( - MEDIA_TYPE_MUSIC, - MEDIA_TYPE_PLAYLIST, -) -from homeassistant.const import ( - CONF_HOST, - CONF_NAME, - CONF_PASSWORD, - CONF_PORT, - STATE_OFF, - STATE_PAUSED, - STATE_PLAYING, -) +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_PORT from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -92,7 +82,7 @@ class CmusRemote: class CmusDevice(MediaPlayerEntity): """Representation of a running cmus.""" - _attr_media_content_type = MEDIA_TYPE_MUSIC + _attr_media_content_type = MediaType.MUSIC _attr_supported_features = ( MediaPlayerEntityFeature.PAUSE | MediaPlayerEntityFeature.VOLUME_SET @@ -128,11 +118,11 @@ class CmusDevice(MediaPlayerEntity): else: self.status = status if self.status.get("status") == "playing": - self._attr_state = STATE_PLAYING + self._attr_state = MediaPlayerState.PLAYING elif self.status.get("status") == "paused": - self._attr_state = STATE_PAUSED + self._attr_state = MediaPlayerState.PAUSED else: - self._attr_state = STATE_OFF + self._attr_state = MediaPlayerState.OFF self._attr_media_content_id = self.status.get("file") self._attr_media_duration = self.status.get("duration") self._attr_media_title = self.status["tag"].get("title") @@ -187,16 +177,18 @@ class CmusDevice(MediaPlayerEntity): if current_volume <= 100: self._remote.cmus.set_volume(int(current_volume) - 5) - def play_media(self, media_type: str, media_id: str, **kwargs: Any) -> None: + def play_media( + self, media_type: MediaType | str, media_id: str, **kwargs: Any + ) -> None: """Send the play command.""" - if media_type in [MEDIA_TYPE_MUSIC, MEDIA_TYPE_PLAYLIST]: + if media_type in {MediaType.MUSIC, MediaType.PLAYLIST}: self._remote.cmus.player_play_file(media_id) else: _LOGGER.error( "Invalid media type %s. Only %s and %s are supported", media_type, - MEDIA_TYPE_MUSIC, - MEDIA_TYPE_PLAYLIST, + MediaType.MUSIC, + MediaType.PLAYLIST, ) def media_pause(self) -> None: diff --git a/homeassistant/components/coinbase/const.py b/homeassistant/components/coinbase/const.py index 08773745f09..48d4b1a6307 100644 --- a/homeassistant/components/coinbase/const.py +++ b/homeassistant/components/coinbase/const.py @@ -110,6 +110,7 @@ WALLETS = { "FJD": "FJD", "FKP": "FKP", "FORTH": "FORTH", + "GALA": "GALA", "GBP": "GBP", "GBX": "GBX", "GEL": "GEL", diff --git a/homeassistant/components/coinbase/translations/pt.json b/homeassistant/components/coinbase/translations/pt.json index 49cb628dd85..dec932ffe10 100644 --- a/homeassistant/components/coinbase/translations/pt.json +++ b/homeassistant/components/coinbase/translations/pt.json @@ -1,7 +1,23 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, "error": { "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "api_key": "Chave da API" + } + } + } + }, + "options": { + "error": { "unknown": "Erro inesperado" } } diff --git a/homeassistant/components/comfoconnect/sensor.py b/homeassistant/components/comfoconnect/sensor.py index 2be98b9eeb5..efb6b11a6d9 100644 --- a/homeassistant/components/comfoconnect/sensor.py +++ b/homeassistant/components/comfoconnect/sensor.py @@ -41,6 +41,7 @@ from homeassistant.const import ( ENERGY_KILO_WATT_HOUR, PERCENTAGE, POWER_WATT, + REVOLUTIONS_PER_MINUTE, TEMP_CELSIUS, TIME_DAYS, VOLUME_FLOW_RATE_CUBIC_METERS_PER_HOUR, @@ -159,7 +160,7 @@ SENSOR_TYPES = ( key=ATTR_SUPPLY_FAN_SPEED, state_class=SensorStateClass.MEASUREMENT, name="Supply fan speed", - native_unit_of_measurement="rpm", + native_unit_of_measurement=REVOLUTIONS_PER_MINUTE, icon="mdi:fan-plus", sensor_id=SENSOR_FAN_SUPPLY_SPEED, ), @@ -175,7 +176,7 @@ SENSOR_TYPES = ( key=ATTR_EXHAUST_FAN_SPEED, state_class=SensorStateClass.MEASUREMENT, name="Exhaust fan speed", - native_unit_of_measurement="rpm", + native_unit_of_measurement=REVOLUTIONS_PER_MINUTE, icon="mdi:fan-minus", sensor_id=SENSOR_FAN_EXHAUST_SPEED, ), diff --git a/homeassistant/components/config/config_entries.py b/homeassistant/components/config/config_entries.py index 54132080f08..fb96a99827d 100644 --- a/homeassistant/components/config/config_entries.py +++ b/homeassistant/components/config/config_entries.py @@ -20,6 +20,7 @@ from homeassistant.helpers.data_entry_flow import ( FlowManagerIndexView, FlowManagerResourceView, ) +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.loader import ( Integration, IntegrationNotFound, @@ -43,6 +44,7 @@ async def async_setup(hass): websocket_api.async_register_command(hass, config_entries_get) websocket_api.async_register_command(hass, config_entry_disable) websocket_api.async_register_command(hass, config_entry_update) + websocket_api.async_register_command(hass, config_entries_subscribe) websocket_api.async_register_command(hass, config_entries_progress) websocket_api.async_register_command(hass, ignore_config_flow) @@ -408,6 +410,54 @@ async def config_entries_get( ) +@websocket_api.websocket_command( + { + vol.Required("type"): "config_entries/subscribe", + vol.Optional("type_filter"): str, + } +) +@websocket_api.async_response +async def config_entries_subscribe( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: + """Subscribe to config entry updates.""" + type_filter = msg.get("type_filter") + + async def async_forward_config_entry_changes( + change: config_entries.ConfigEntryChange, entry: config_entries.ConfigEntry + ) -> None: + """Forward config entry state events to websocket.""" + if type_filter: + integration = await async_get_integration(hass, entry.domain) + if integration.integration_type != type_filter: + return + + connection.send_message( + websocket_api.event_message( + msg["id"], + [ + { + "type": change, + "entry": entry_json(entry), + } + ], + ) + ) + + current_entries = await async_matching_config_entries(hass, type_filter, None) + connection.subscriptions[msg["id"]] = async_dispatcher_connect( + hass, + config_entries.SIGNAL_CONFIG_ENTRY_CHANGED, + async_forward_config_entry_changes, + ) + connection.send_result(msg["id"]) + connection.send_message( + websocket_api.event_message( + msg["id"], [{"type": None, "entry": entry} for entry in current_entries] + ) + ) + + async def async_matching_config_entries( hass: HomeAssistant, type_filter: str | None, domain: str | None ) -> list[dict[str, Any]]: diff --git a/homeassistant/components/config/device_registry.py b/homeassistant/components/config/device_registry.py index 587710a8c2a..8edd9f1f4d3 100644 --- a/homeassistant/components/config/device_registry.py +++ b/homeassistant/components/config/device_registry.py @@ -160,6 +160,7 @@ def _entry_dict(entry): "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, @@ -167,6 +168,5 @@ def _entry_dict(entry): "name_by_user": entry.name_by_user, "name": entry.name, "sw_version": entry.sw_version, - "hw_version": entry.hw_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 cbfd092bc0c..b024c3f0128 100644 --- a/homeassistant/components/config/entity_registry.py +++ b/homeassistant/components/config/entity_registry.py @@ -7,7 +7,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.components import websocket_api -from homeassistant.components.websocket_api.const import ERR_NOT_FOUND +from homeassistant.components.websocket_api import ERR_NOT_FOUND from homeassistant.components.websocket_api.decorators import require_admin from homeassistant.components.websocket_api.messages import ( IDEN_JSON_TEMPLATE, @@ -136,22 +136,18 @@ def websocket_update_entity(hass, connection, msg): changes = {} - for key in ("area_id", "device_class", "disabled_by", "hidden_by", "icon", "name"): + for key in ( + "area_id", + "device_class", + "disabled_by", + "hidden_by", + "icon", + "name", + "new_entity_id", + ): if key in msg: changes[key] = msg[key] - if "new_entity_id" in msg and msg["new_entity_id"] != entity_id: - changes["new_entity_id"] = msg["new_entity_id"] - if hass.states.get(msg["new_entity_id"]) is not None: - connection.send_message( - websocket_api.error_message( - msg["id"], - "invalid_info", - "Entity with this ID is already registered", - ) - ) - return - if "disabled_by" in msg and msg["disabled_by"] is None: # Don't allow enabling an entity of a disabled device if entity_entry.device_id: @@ -238,6 +234,7 @@ def _entry_dict(entry: er.RegistryEntry) -> dict[str, Any]: "hidden_by": entry.hidden_by, "icon": entry.icon, "id": entry.id, + "unique_id": entry.unique_id, "name": entry.name, "original_name": entry.original_name, "platform": entry.platform, @@ -253,5 +250,4 @@ def _entry_ext_dict(entry: er.RegistryEntry) -> dict[str, Any]: data["options"] = entry.options data["original_device_class"] = entry.original_device_class data["original_icon"] = entry.original_icon - data["unique_id"] = entry.unique_id return data diff --git a/homeassistant/components/config/manifest.json b/homeassistant/components/config/manifest.json index 57dfd0d360a..3be667f6cd2 100644 --- a/homeassistant/components/config/manifest.json +++ b/homeassistant/components/config/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/config", "dependencies": ["http"], "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "system" } diff --git a/homeassistant/components/configurator/manifest.json b/homeassistant/components/configurator/manifest.json index acd0fa80423..716fe26910b 100644 --- a/homeassistant/components/configurator/manifest.json +++ b/homeassistant/components/configurator/manifest.json @@ -3,5 +3,6 @@ "name": "Configurator", "documentation": "https://www.home-assistant.io/integrations/configurator", "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "system" } diff --git a/homeassistant/components/conversation/manifest.json b/homeassistant/components/conversation/manifest.json index 1d2e0893065..54265bfcb83 100644 --- a/homeassistant/components/conversation/manifest.json +++ b/homeassistant/components/conversation/manifest.json @@ -5,5 +5,6 @@ "dependencies": ["http"], "codeowners": ["@home-assistant/core"], "quality_scale": "internal", - "iot_class": "local_push" + "iot_class": "local_push", + "integration_type": "system" } diff --git a/homeassistant/components/coolmaster/climate.py b/homeassistant/components/coolmaster/climate.py index 8333e66753e..d2b0685cdf0 100644 --- a/homeassistant/components/coolmaster/climate.py +++ b/homeassistant/components/coolmaster/climate.py @@ -2,8 +2,11 @@ import logging from typing import Any -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ClimateEntityFeature, HVACMode +from homeassistant.components.climate import ( + ClimateEntity, + ClimateEntityFeature, + HVACMode, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.core import HomeAssistant, callback diff --git a/homeassistant/components/coolmaster/config_flow.py b/homeassistant/components/coolmaster/config_flow.py index 2c5592c156b..9ad88d36574 100644 --- a/homeassistant/components/coolmaster/config_flow.py +++ b/homeassistant/components/coolmaster/config_flow.py @@ -6,7 +6,7 @@ from typing import Any from pycoolmasternet_async import CoolMasterNet import voluptuous as vol -from homeassistant.components.climate.const import HVACMode +from homeassistant.components.climate import HVACMode from homeassistant.config_entries import ConfigFlow from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.core import callback diff --git a/homeassistant/components/coolmaster/translations/cs.json b/homeassistant/components/coolmaster/translations/cs.json index 9e820808f0c..2d459900450 100644 --- a/homeassistant/components/coolmaster/translations/cs.json +++ b/homeassistant/components/coolmaster/translations/cs.json @@ -10,7 +10,7 @@ "cool": "Podpora re\u017eimu chlazen\u00ed", "dry": "Podpora re\u017eimu vysou\u0161en\u00ed", "fan_only": "Podpora re\u017eimu pouze ventil\u00e1tor", - "heat": "Podpora re\u017eimu topen\u00ed", + "heat": "Podpora re\u017eimu vyt\u00e1p\u011bn\u00ed", "heat_cool": "Podpora automatick\u00e9ho oh\u0159\u00edv\u00e1n\u00ed/chlazen\u00ed", "host": "Hostitel", "off": "Lze vypnout" diff --git a/homeassistant/components/coronavirus/translations/pt.json b/homeassistant/components/coronavirus/translations/pt.json index e03867478c4..308eaef73f0 100644 --- a/homeassistant/components/coronavirus/translations/pt.json +++ b/homeassistant/components/coronavirus/translations/pt.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado", + "cannot_connect": "Falha na liga\u00e7\u00e3o" }, "step": { "user": { diff --git a/homeassistant/components/counter/__init__.py b/homeassistant/components/counter/__init__.py index 83d307f69d3..dedeb428c0c 100644 --- a/homeassistant/components/counter/__init__.py +++ b/homeassistant/components/counter/__init__.py @@ -93,14 +93,14 @@ CONFIG_SCHEMA = vol.Schema( async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the counters.""" - component = EntityComponent(_LOGGER, DOMAIN, hass) + component = EntityComponent[Counter](_LOGGER, DOMAIN, hass) id_manager = collection.IDManager() yaml_collection = collection.YamlCollection( logging.getLogger(f"{__name__}.yaml_collection"), id_manager ) collection.sync_entity_lifecycle( - hass, DOMAIN, DOMAIN, component, yaml_collection, Counter.from_yaml + hass, DOMAIN, DOMAIN, component, yaml_collection, Counter ) storage_collection = CounterStorageCollection( @@ -159,19 +159,26 @@ class CounterStorageCollection(collection.StorageCollection): return {CONF_ID: data[CONF_ID]} | update_data -class Counter(RestoreEntity): +class Counter(collection.CollectionEntity, RestoreEntity): """Representation of a counter.""" _attr_should_poll: bool = False + editable: bool - def __init__(self, config: dict) -> None: + def __init__(self, config: ConfigType) -> None: """Initialize a counter.""" - self._config: dict = config + self._config: ConfigType = config self._state: int | None = config[CONF_INITIAL] - self.editable: bool = True @classmethod - def from_yaml(cls, config: dict) -> Counter: + def from_storage(cls, config: ConfigType) -> Counter: + """Create counter instance from storage.""" + counter = cls(config) + counter.editable = True + return counter + + @classmethod + def from_yaml(cls, config: ConfigType) -> Counter: """Create counter instance from yaml config.""" counter = cls(config) counter.editable = False @@ -262,7 +269,7 @@ class Counter(RestoreEntity): self._state = self.compute_next_state(new_state) self.async_write_ha_state() - async def async_update_config(self, config: dict) -> None: + async def async_update_config(self, config: ConfigType) -> None: """Change the counter's settings WS CRUD.""" self._config = config self._state = self.compute_next_state(self._state) diff --git a/homeassistant/components/cover/__init__.py b/homeassistant/components/cover/__init__.py index b66398b3491..57187c56819 100644 --- a/homeassistant/components/cover/__init__.py +++ b/homeassistant/components/cover/__init__.py @@ -83,6 +83,8 @@ DEVICE_CLASS_SHADE = CoverDeviceClass.SHADE.value DEVICE_CLASS_SHUTTER = CoverDeviceClass.SHUTTER.value DEVICE_CLASS_WINDOW = CoverDeviceClass.WINDOW.value +# mypy: disallow-any-generics + class CoverEntityFeature(IntEnum): """Supported features of the cover entity.""" @@ -122,7 +124,7 @@ def is_closed(hass: HomeAssistant, entity_id: str) -> bool: async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Track states and offer events for covers.""" - component = hass.data[DOMAIN] = EntityComponent( + component = hass.data[DOMAIN] = EntityComponent[CoverEntity]( _LOGGER, DOMAIN, hass, SCAN_INTERVAL ) @@ -202,13 +204,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry.""" - component: EntityComponent = hass.data[DOMAIN] + component: EntityComponent[CoverEntity] = hass.data[DOMAIN] return await component.async_setup_entry(entry) async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - component: EntityComponent = hass.data[DOMAIN] + component: EntityComponent[CoverEntity] = hass.data[DOMAIN] return await component.async_unload_entry(entry) diff --git a/homeassistant/components/cover/manifest.json b/homeassistant/components/cover/manifest.json index 3da130fd799..66347b77eea 100644 --- a/homeassistant/components/cover/manifest.json +++ b/homeassistant/components/cover/manifest.json @@ -3,5 +3,6 @@ "name": "Cover", "documentation": "https://www.home-assistant.io/integrations/cover", "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "entity" } diff --git a/homeassistant/components/cover/translations/hu.json b/homeassistant/components/cover/translations/hu.json index 2155907cae2..e2f24e28af7 100644 --- a/homeassistant/components/cover/translations/hu.json +++ b/homeassistant/components/cover/translations/hu.json @@ -35,5 +35,5 @@ "stopped": "Meg\u00e1llt" } }, - "title": "Bor\u00edt\u00f3" + "title": "\u00c1rny\u00e9kol\u00f3" } \ No newline at end of file diff --git a/homeassistant/components/cppm_tracker/device_tracker.py b/homeassistant/components/cppm_tracker/device_tracker.py index 8984da6beed..0c7acd33f23 100644 --- a/homeassistant/components/cppm_tracker/device_tracker.py +++ b/homeassistant/components/cppm_tracker/device_tracker.py @@ -32,7 +32,7 @@ PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( _LOGGER = logging.getLogger(__name__) -def get_scanner(hass: HomeAssistant, config: ConfigType) -> DeviceScanner | None: +def get_scanner(hass: HomeAssistant, config: ConfigType) -> CPPMDeviceScanner | None: """Initialize Scanner.""" data = { diff --git a/homeassistant/components/cpuspeed/translations/pt.json b/homeassistant/components/cpuspeed/translations/pt.json index cf03f249d96..f2a86ca8ca8 100644 --- a/homeassistant/components/cpuspeed/translations/pt.json +++ b/homeassistant/components/cpuspeed/translations/pt.json @@ -2,6 +2,12 @@ "config": { "abort": { "already_configured": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "step": { + "user": { + "description": "Quer dar inicio \u00e0 configura\u00e7\u00e3o?", + "title": "Velocidade da CPU" + } } } } \ No newline at end of file diff --git a/homeassistant/components/crownstone/const.py b/homeassistant/components/crownstone/const.py index a362435b9ce..9b3624a4575 100644 --- a/homeassistant/components/crownstone/const.py +++ b/homeassistant/components/crownstone/const.py @@ -7,6 +7,7 @@ from homeassistant.const import Platform # Platforms DOMAIN: Final = "crownstone" +PROJECT_NAME: Final = "home-assistant-core" PLATFORMS: Final[list[Platform]] = [Platform.LIGHT] # Listeners diff --git a/homeassistant/components/crownstone/entry_manager.py b/homeassistant/components/crownstone/entry_manager.py index dcae7ef4705..2f74daa8629 100644 --- a/homeassistant/components/crownstone/entry_manager.py +++ b/homeassistant/components/crownstone/entry_manager.py @@ -27,6 +27,7 @@ from .const import ( CONF_USB_SPHERE, DOMAIN, PLATFORMS, + PROJECT_NAME, SSE_LISTENERS, UART_LISTENERS, ) @@ -84,6 +85,7 @@ class CrownstoneEntryManager: password=password, access_token=self.cloud.access_token, websession=aiohttp_client.async_create_clientsession(self.hass), + project_name=PROJECT_NAME, ) # Listen for events in the background, without task tracking asyncio.create_task(self.async_process_events(self.sse)) diff --git a/homeassistant/components/crownstone/manifest.json b/homeassistant/components/crownstone/manifest.json index cdc79e7f0b5..39abd998be7 100644 --- a/homeassistant/components/crownstone/manifest.json +++ b/homeassistant/components/crownstone/manifest.json @@ -5,7 +5,7 @@ "documentation": "https://www.home-assistant.io/integrations/crownstone", "requirements": [ "crownstone-cloud==1.4.9", - "crownstone-sse==2.0.3", + "crownstone-sse==2.0.4", "crownstone-uart==2.1.0", "pyserial==3.5" ], diff --git a/homeassistant/components/crownstone/translations/pt.json b/homeassistant/components/crownstone/translations/pt.json index 97ea705b32b..5a97b161335 100644 --- a/homeassistant/components/crownstone/translations/pt.json +++ b/homeassistant/components/crownstone/translations/pt.json @@ -1,6 +1,14 @@ { "config": { + "error": { + "unknown": "Erro inesperado" + }, "step": { + "usb_config": { + "data": { + "usb_path": "Caminho do Dispositivo USB" + } + }, "usb_manual_config": { "data": { "usb_manual_path": "Caminho do Dispositivo USB" @@ -8,9 +16,24 @@ }, "user": { "data": { + "email": "Email", "password": "Palavra-passe" } } } + }, + "options": { + "step": { + "usb_config": { + "data": { + "usb_path": "Caminho do Dispositivo USB" + } + }, + "usb_manual_config": { + "data": { + "usb_manual_path": "Caminho do Dispositivo USB" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/daikin/climate.py b/homeassistant/components/daikin/climate.py index 2f07c5a0bdc..02107205e70 100644 --- a/homeassistant/components/daikin/climate.py +++ b/homeassistant/components/daikin/climate.py @@ -6,16 +6,17 @@ from typing import Any import voluptuous as vol -from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( ATTR_FAN_MODE, ATTR_HVAC_MODE, ATTR_PRESET_MODE, ATTR_SWING_MODE, + PLATFORM_SCHEMA, PRESET_AWAY, PRESET_BOOST, PRESET_ECO, PRESET_NONE, + ClimateEntity, ClimateEntityFeature, HVACAction, HVACMode, diff --git a/homeassistant/components/daikin/translations/cs.json b/homeassistant/components/daikin/translations/cs.json index 86625365a30..e7906d0af45 100644 --- a/homeassistant/components/daikin/translations/cs.json +++ b/homeassistant/components/daikin/translations/cs.json @@ -5,6 +5,7 @@ "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" }, "error": { + "api_password": "Neplatn\u00e9 ov\u011b\u0159en\u00ed, pou\u017eijte kl\u00ed\u010d API nebo heslo.", "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" @@ -16,7 +17,7 @@ "host": "Hostitel", "password": "Heslo" }, - "description": "Zadejte IP adresu va\u0161eho Daikin AC. \n\nV\u0161imn\u011bte si, \u017ee Kl\u00ed\u010d API a Heslo jsou pou\u017eit\u00e9 za\u0159\u00edzen\u00edm BRP072Cxx, respektive SKYFi.", + "description": "Zadejte IP adresu va\u0161eho Daikin AC. \n\nVezm\u011bte na v\u011bdom\u00ed, \u017ee Kl\u00ed\u010d API a Heslo pou\u017e\u00edvaj\u00ed pouze za\u0159\u00edzen\u00ed BRP072Cxx a SKYFi.", "title": "Nastaven\u00ed Daikin AC" } } diff --git a/homeassistant/components/daikin/translations/pt.json b/homeassistant/components/daikin/translations/pt.json index 28cdb0596d7..0d1c8f842c0 100644 --- a/homeassistant/components/daikin/translations/pt.json +++ b/homeassistant/components/daikin/translations/pt.json @@ -5,6 +5,7 @@ "cannot_connect": "Falha na liga\u00e7\u00e3o" }, "error": { + "api_password": "Autentica\u00e7\u00e3o inv\u00e1lida, use a chave de API ou a palavra-passe.", "cannot_connect": "Falha na liga\u00e7\u00e3o", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "unknown": "Erro inesperado" diff --git a/homeassistant/components/ddwrt/device_tracker.py b/homeassistant/components/ddwrt/device_tracker.py index 0947b755470..ba34ec48e0f 100644 --- a/homeassistant/components/ddwrt/device_tracker.py +++ b/homeassistant/components/ddwrt/device_tracker.py @@ -46,7 +46,7 @@ PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( ) -def get_scanner(hass: HomeAssistant, config: ConfigType) -> DeviceScanner | None: +def get_scanner(hass: HomeAssistant, config: ConfigType) -> DdWrtDeviceScanner | None: """Validate the configuration and return a DD-WRT scanner.""" try: return DdWrtDeviceScanner(config[DOMAIN]) diff --git a/homeassistant/components/deconz/climate.py b/homeassistant/components/deconz/climate.py index e49918e14f2..0d13f2639da 100644 --- a/homeassistant/components/deconz/climate.py +++ b/homeassistant/components/deconz/climate.py @@ -11,8 +11,8 @@ from pydeconz.models.sensor.thermostat import ( ThermostatPreset, ) -from homeassistant.components.climate import DOMAIN, ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( + DOMAIN, FAN_AUTO, FAN_HIGH, FAN_LOW, @@ -22,7 +22,9 @@ from homeassistant.components.climate.const import ( PRESET_BOOST, PRESET_COMFORT, PRESET_ECO, + ClimateEntity, ClimateEntityFeature, + HVACAction, HVACMode, ) from homeassistant.config_entries import ConfigEntry @@ -171,6 +173,21 @@ class DeconzThermostat(DeconzDevice[Thermostat], ClimateEntity): mode=HVAC_MODE_TO_DECONZ[hvac_mode], ) + @property + def hvac_action(self) -> str | None: + """Return current hvac operation ie. heat, cool. + + Preset 'BOOST' is interpreted as 'state_on'. + """ + if self._device.mode == ThermostatMode.OFF: + return HVACAction.OFF + + if self._device.state_on or self._device.preset == ThermostatPreset.BOOST: + if self._device.mode == ThermostatMode.COOL: + return HVACAction.COOLING + return HVACAction.HEATING + return HVACAction.IDLE + # Preset control @property diff --git a/homeassistant/components/deconz/logbook.py b/homeassistant/components/deconz/logbook.py index 07dc7cb0124..65ed7f8e31d 100644 --- a/homeassistant/components/deconz/logbook.py +++ b/homeassistant/components/deconz/logbook.py @@ -3,10 +3,7 @@ from __future__ import annotations from collections.abc import Callable -from homeassistant.components.logbook.const import ( - LOGBOOK_ENTRY_MESSAGE, - LOGBOOK_ENTRY_NAME, -) +from homeassistant.components.logbook import LOGBOOK_ENTRY_MESSAGE, LOGBOOK_ENTRY_NAME from homeassistant.const import ATTR_DEVICE_ID, CONF_EVENT from homeassistant.core import Event, HomeAssistant, callback import homeassistant.helpers.device_registry as dr diff --git a/homeassistant/components/default_config/manifest.json b/homeassistant/components/default_config/manifest.json index 6701e62c71f..d33aee6e030 100644 --- a/homeassistant/components/default_config/manifest.json +++ b/homeassistant/components/default_config/manifest.json @@ -41,5 +41,6 @@ "zone" ], "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "system" } diff --git a/homeassistant/components/demo/__init__.py b/homeassistant/components/demo/__init__.py index cc80edbd484..7ed989903e5 100644 --- a/homeassistant/components/demo/__init__.py +++ b/homeassistant/components/demo/__init__.py @@ -16,8 +16,13 @@ from homeassistant.components.recorder.statistics import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ENTITY_ID, + ENERGY_KILO_WATT_HOUR, + ENERGY_MEGA_WATT_HOUR, EVENT_HOMEASSISTANT_START, SOUND_PRESSURE_DB, + TEMP_CELSIUS, + VOLUME_CUBIC_FEET, + VOLUME_CUBIC_METERS, Platform, ) import homeassistant.core as ha @@ -255,14 +260,14 @@ async def _insert_sum_statistics( 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, True + get_last_statistics, hass, 1, statistic_id, False ) if statistic_id in last_stats: sum_ = last_stats[statistic_id][0]["sum"] or 0 @@ -291,7 +296,7 @@ async def _insert_statistics(hass: HomeAssistant) -> None: "source": DOMAIN, "name": "Outdoor temperature", "statistic_id": f"{DOMAIN}:temperature_outdoor", - "unit_of_measurement": "°C", + "unit_of_measurement": TEMP_CELSIUS, "has_mean": True, "has_sum": False, } @@ -304,11 +309,11 @@ async def _insert_statistics(hass: HomeAssistant) -> None: "source": DOMAIN, "name": "Energy consumption 1", "statistic_id": f"{DOMAIN}:energy_consumption_kwh", - "unit_of_measurement": "kWh", + "unit_of_measurement": ENERGY_KILO_WATT_HOUR, "has_mean": False, "has_sum": True, } - await _insert_sum_statistics(hass, metadata, yesterday_midnight, today_midnight, 2) + 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 @@ -316,12 +321,12 @@ async def _insert_statistics(hass: HomeAssistant) -> None: "source": DOMAIN, "name": "Energy consumption 2", "statistic_id": f"{DOMAIN}:energy_consumption_mwh", - "unit_of_measurement": "MWh", + "unit_of_measurement": ENERGY_MEGA_WATT_HOUR, "has_mean": False, "has_sum": True, } await _insert_sum_statistics( - hass, metadata, yesterday_midnight, today_midnight, 0.002 + hass, metadata, yesterday_midnight, today_midnight, 0.001 ) # Add external gas consumption in m³, ~6 m3/day @@ -330,11 +335,13 @@ async def _insert_statistics(hass: HomeAssistant) -> None: "source": DOMAIN, "name": "Gas consumption 1", "statistic_id": f"{DOMAIN}:gas_consumption_m3", - "unit_of_measurement": "m³", + "unit_of_measurement": VOLUME_CUBIC_METERS, "has_mean": False, "has_sum": True, } - await _insert_sum_statistics(hass, metadata, yesterday_midnight, today_midnight, 1) + 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 @@ -342,11 +349,11 @@ async def _insert_statistics(hass: HomeAssistant) -> None: "source": DOMAIN, "name": "Gas consumption 2", "statistic_id": f"{DOMAIN}:gas_consumption_ft3", - "unit_of_measurement": "ft³", + "unit_of_measurement": VOLUME_CUBIC_FEET, "has_mean": False, "has_sum": True, } - await _insert_sum_statistics(hass, metadata, yesterday_midnight, today_midnight, 30) + await _insert_sum_statistics(hass, metadata, yesterday_midnight, today_midnight, 15) async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/demo/alarm_control_panel.py b/homeassistant/components/demo/alarm_control_panel.py index b73e5444b22..3a94aaa7c29 100644 --- a/homeassistant/components/demo/alarm_control_panel.py +++ b/homeassistant/components/demo/alarm_control_panel.py @@ -31,7 +31,7 @@ async def async_setup_platform( """Set up the Demo alarm control panel platform.""" async_add_entities( [ - ManualAlarm( + ManualAlarm( # type:ignore[no-untyped-call] hass, "Security", "1234", diff --git a/homeassistant/components/demo/binary_sensor.py b/homeassistant/components/demo/binary_sensor.py index 584c0cf88f1..ee718e85cc0 100644 --- a/homeassistant/components/demo/binary_sensor.py +++ b/homeassistant/components/demo/binary_sensor.py @@ -61,7 +61,7 @@ class DemoBinarySensor(BinarySensorEntity): self._unique_id = unique_id self._attr_name = name self._state = state - self._sensor_type = device_class + self._attr_device_class = device_class @property def device_info(self) -> DeviceInfo: @@ -79,11 +79,6 @@ class DemoBinarySensor(BinarySensorEntity): """Return the unique id.""" return self._unique_id - @property - def device_class(self) -> BinarySensorDeviceClass: - """Return the class of this sensor.""" - return self._sensor_type - @property def is_on(self) -> bool: """Return true if the binary sensor is on.""" diff --git a/homeassistant/components/demo/calendar.py b/homeassistant/components/demo/calendar.py index 415ed0dbb8d..ae546361d8f 100644 --- a/homeassistant/components/demo/calendar.py +++ b/homeassistant/components/demo/calendar.py @@ -1,16 +1,9 @@ """Demo platform that has two fake binary sensors.""" from __future__ import annotations -import copy import datetime -from typing import Any -from homeassistant.components.calendar import ( - CalendarEntity, - CalendarEvent, - CalendarEventDevice, - get_date, -) +from homeassistant.components.calendar import CalendarEntity, CalendarEvent from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -28,7 +21,6 @@ def setup_platform( [ DemoCalendar(calendar_data_future(), "Calendar 1"), DemoCalendar(calendar_data_current(), "Calendar 2"), - LegacyDemoCalendar("Calendar 3"), ] ) @@ -76,41 +68,3 @@ class DemoCalendar(CalendarEntity): ) -> list[CalendarEvent]: """Return calendar events within a datetime range.""" return [self._event] - - -class LegacyDemoCalendar(CalendarEventDevice): - """Calendar for exercising shim API.""" - - def __init__(self, name: str) -> None: - """Initialize demo calendar.""" - self._attr_name = name - one_hour_from_now = dt_util.now() + datetime.timedelta(minutes=30) - self._event = { - "start": {"dateTime": one_hour_from_now.isoformat()}, - "end": { - "dateTime": ( - one_hour_from_now + datetime.timedelta(minutes=60) - ).isoformat() - }, - "summary": "Future Event", - "description": "Future Description", - "location": "Future Location", - } - - @property - def event(self) -> dict[str, Any]: - """Return the next upcoming event.""" - return self._event - - async def async_get_events( - self, - hass: HomeAssistant, - start_date: datetime.datetime, - end_date: datetime.datetime, - ) -> list[dict[str, Any]]: - """Get all events in a specific time frame.""" - event = copy.copy(self.event) - event["title"] = event["summary"] - event["start"] = get_date(event["start"]).isoformat() - event["end"] = get_date(event["end"]).isoformat() - return [event] diff --git a/homeassistant/components/demo/climate.py b/homeassistant/components/demo/climate.py index 546a580f576..91594423744 100644 --- a/homeassistant/components/demo/climate.py +++ b/homeassistant/components/demo/climate.py @@ -3,10 +3,10 @@ from __future__ import annotations from typing import Any -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, + ClimateEntity, ClimateEntityFeature, HVACAction, HVACMode, diff --git a/homeassistant/components/demo/cover.py b/homeassistant/components/demo/cover.py index fbb8a171516..845bd9976a3 100644 --- a/homeassistant/components/demo/cover.py +++ b/homeassistant/components/demo/cover.py @@ -85,7 +85,7 @@ class DemoCover(CoverEntity): self._unique_id = unique_id self._attr_name = name self._position = position - self._device_class = device_class + self._attr_device_class = device_class self._supported_features = supported_features self._set_position: int | None = None self._set_tilt_position: int | None = None @@ -142,11 +142,6 @@ class DemoCover(CoverEntity): """Return if the cover is opening.""" return self._is_opening - @property - def device_class(self) -> CoverDeviceClass | None: - """Return the class of this device, from component DEVICE_CLASSES.""" - return self._device_class - @property def supported_features(self) -> int: """Flag supported features.""" diff --git a/homeassistant/components/demo/humidifier.py b/homeassistant/components/demo/humidifier.py index c998a32ab55..571cfbe8db9 100644 --- a/homeassistant/components/demo/humidifier.py +++ b/homeassistant/components/demo/humidifier.py @@ -3,8 +3,11 @@ from __future__ import annotations from typing import Any -from homeassistant.components.humidifier import HumidifierDeviceClass, HumidifierEntity -from homeassistant.components.humidifier.const import HumidifierEntityFeature +from homeassistant.components.humidifier import ( + HumidifierDeviceClass, + HumidifierEntity, + HumidifierEntityFeature, +) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback diff --git a/homeassistant/components/demo/image_processing.py b/homeassistant/components/demo/image_processing.py index 6ba498114a4..dc5565a1771 100644 --- a/homeassistant/components/demo/image_processing.py +++ b/homeassistant/components/demo/image_processing.py @@ -1,12 +1,8 @@ """Support for the demo image processing.""" from __future__ import annotations -from homeassistant.components.camera import Image from homeassistant.components.image_processing import ( - ATTR_AGE, - ATTR_CONFIDENCE, - ATTR_GENDER, - ATTR_NAME, + FaceInformation, ImageProcessingFaceEntity, ) from homeassistant.components.openalpr_local.image_processing import ( @@ -52,7 +48,7 @@ class DemoImageProcessingAlpr(ImageProcessingAlprEntity): """Return minimum confidence for send events.""" return 80 - def process_image(self, image: Image) -> None: + def process_image(self, image: bytes) -> None: """Process image.""" demo_data = { "AC3829": 98.3, @@ -84,17 +80,17 @@ class DemoImageProcessingFace(ImageProcessingFaceEntity): """Return minimum confidence for send events.""" return 80 - def process_image(self, image: Image) -> None: + def process_image(self, image: bytes) -> None: """Process image.""" demo_data = [ - { - ATTR_CONFIDENCE: 98.34, - ATTR_NAME: "Hans", - ATTR_AGE: 16.0, - ATTR_GENDER: "male", - }, - {ATTR_NAME: "Helena", ATTR_AGE: 28.0, ATTR_GENDER: "female"}, - {ATTR_CONFIDENCE: 62.53, ATTR_NAME: "Luna"}, + FaceInformation( + confidence=98.34, + name="Hans", + age=16.0, + gender="male", + ), + FaceInformation(name="Helena", age=28.0, gender="female"), + FaceInformation(confidence=62.53, name="Luna"), ] self.process_faces(demo_data, 4) diff --git a/homeassistant/components/demo/mailbox.py b/homeassistant/components/demo/mailbox.py index 8a7df70df80..6f0b23525e5 100644 --- a/homeassistant/components/demo/mailbox.py +++ b/homeassistant/components/demo/mailbox.py @@ -76,7 +76,7 @@ class DemoMailbox(Mailbox): """Return a list of the current messages.""" return sorted( self._messages.values(), - key=lambda item: item["info"]["origtime"], + key=lambda item: item["info"]["origtime"], # type: ignore[no-any-return] reverse=True, ) diff --git a/homeassistant/components/demo/media_player.py b/homeassistant/components/demo/media_player.py index d5098dc4586..8bbe380d9ec 100644 --- a/homeassistant/components/demo/media_player.py +++ b/homeassistant/components/demo/media_player.py @@ -7,16 +7,12 @@ from typing import Any from homeassistant.components.media_player import ( MediaPlayerDeviceClass, MediaPlayerEntity, -) -from homeassistant.components.media_player.const import ( - MEDIA_TYPE_MOVIE, - MEDIA_TYPE_MUSIC, - MEDIA_TYPE_TVSHOW, - REPEAT_MODE_OFF, MediaPlayerEntityFeature, + MediaPlayerState, + MediaType, + RepeatMode, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import STATE_OFF, STATE_PAUSED, STATE_PLAYING from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -109,6 +105,7 @@ NETFLIX_PLAYER_SUPPORT = ( class AbstractDemoPlayer(MediaPlayerEntity): """A demo media players.""" + _attr_sound_mode_list = SOUND_MODE_LIST _attr_should_poll = False # We only implement the methods that we support @@ -118,102 +115,68 @@ class AbstractDemoPlayer(MediaPlayerEntity): ) -> None: """Initialize the demo device.""" self._attr_name = name - self._player_state = STATE_PLAYING - self._volume_level = 1.0 - self._volume_muted = False - self._shuffle = False - self._sound_mode_list = SOUND_MODE_LIST - self._sound_mode = DEFAULT_SOUND_MODE - self._device_class = device_class - - @property - def state(self) -> str: - """Return the state of the player.""" - return self._player_state - - @property - def volume_level(self) -> float: - """Return the volume level of the media player (0..1).""" - return self._volume_level - - @property - def is_volume_muted(self) -> bool: - """Return boolean if volume is currently muted.""" - return self._volume_muted - - @property - def shuffle(self) -> bool: - """Boolean if shuffling is enabled.""" - return self._shuffle - - @property - def sound_mode(self) -> str: - """Return the current sound mode.""" - return self._sound_mode - - @property - def sound_mode_list(self) -> list[str]: - """Return a list of available sound modes.""" - return self._sound_mode_list - - @property - def device_class(self) -> MediaPlayerDeviceClass | None: - """Return the device class of the media player.""" - return self._device_class + self._attr_state = MediaPlayerState.PLAYING + self._attr_volume_level = 1.0 + self._attr_is_volume_muted = False + self._attr_shuffle = False + self._attr_sound_mode = DEFAULT_SOUND_MODE + self._attr_device_class = device_class def turn_on(self) -> None: """Turn the media player on.""" - self._player_state = STATE_PLAYING + self._attr_state = MediaPlayerState.PLAYING self.schedule_update_ha_state() def turn_off(self) -> None: """Turn the media player off.""" - self._player_state = STATE_OFF + self._attr_state = MediaPlayerState.OFF self.schedule_update_ha_state() def mute_volume(self, mute: bool) -> None: """Mute the volume.""" - self._volume_muted = mute + self._attr_is_volume_muted = mute self.schedule_update_ha_state() def volume_up(self) -> None: """Increase volume.""" - self._volume_level = min(1.0, self._volume_level + 0.1) + assert self.volume_level is not None + self._attr_volume_level = min(1.0, self.volume_level + 0.1) self.schedule_update_ha_state() def volume_down(self) -> None: """Decrease volume.""" - self._volume_level = max(0.0, self._volume_level - 0.1) + assert self.volume_level is not None + self._attr_volume_level = max(0.0, self.volume_level - 0.1) self.schedule_update_ha_state() def set_volume_level(self, volume: float) -> None: """Set the volume level, range 0..1.""" - self._volume_level = volume + self._attr_volume_level = volume self.schedule_update_ha_state() def media_play(self) -> None: """Send play command.""" - self._player_state = STATE_PLAYING + self._attr_state = MediaPlayerState.PLAYING self.schedule_update_ha_state() def media_pause(self) -> None: """Send pause command.""" - self._player_state = STATE_PAUSED + self._attr_state = MediaPlayerState.PAUSED self.schedule_update_ha_state() def media_stop(self) -> None: """Send stop command.""" - self._player_state = STATE_OFF + self._attr_state = MediaPlayerState.OFF self.schedule_update_ha_state() def set_shuffle(self, shuffle: bool) -> None: """Enable/disable shuffle mode.""" - self._shuffle = shuffle + self._attr_shuffle = shuffle self.schedule_update_ha_state() def select_sound_mode(self, sound_mode: str) -> None: """Select sound mode.""" - self._sound_mode = sound_mode + self._attr_sound_mode = sound_mode self.schedule_update_ha_state() @@ -222,6 +185,8 @@ class DemoYoutubePlayer(AbstractDemoPlayer): # We only implement the methods that we support + _attr_media_content_type = MediaType.MOVIE + def __init__( self, name: str, youtube_id: str, media_title: str, duration: int ) -> None: @@ -238,11 +203,6 @@ class DemoYoutubePlayer(AbstractDemoPlayer): """Return the content ID of current playing media.""" return self.youtube_id - @property - def media_content_type(self) -> str: - """Return the content type of current playing media.""" - return MEDIA_TYPE_MOVIE - @property def media_duration(self) -> int: """Return the duration of current playing media in seconds.""" @@ -276,7 +236,7 @@ class DemoYoutubePlayer(AbstractDemoPlayer): position = self._progress - if self._player_state == STATE_PLAYING: + if self.state == MediaPlayerState.PLAYING: position += int( (dt_util.utcnow() - self._progress_updated_at).total_seconds() ) @@ -289,7 +249,7 @@ class DemoYoutubePlayer(AbstractDemoPlayer): Returns value from homeassistant.util.dt.utcnow(). """ - if self._player_state == STATE_PLAYING: + if self.state == MediaPlayerState.PLAYING: return self._progress_updated_at return None @@ -310,6 +270,8 @@ class DemoMusicPlayer(AbstractDemoPlayer): # We only implement the methods that we support + _attr_media_content_type = MediaType.MUSIC + tracks = [ ("Technohead", "I Wanna Be A Hippy (Flamman & Abraxas Radio Mix)"), ("Paul Elstak", "Luv U More"), @@ -338,7 +300,7 @@ class DemoMusicPlayer(AbstractDemoPlayer): super().__init__(name) self._cur_track = 0 self._group_members: list[str] = [] - self._repeat = REPEAT_MODE_OFF + self._repeat = RepeatMode.OFF @property def group_members(self) -> list[str]: @@ -350,11 +312,6 @@ class DemoMusicPlayer(AbstractDemoPlayer): """Return the content ID of current playing media.""" return "bounzz-1" - @property - def media_content_type(self) -> str: - """Return the content type of current playing media.""" - return MEDIA_TYPE_MUSIC - @property def media_duration(self) -> int: """Return the duration of current playing media in seconds.""" @@ -386,7 +343,7 @@ class DemoMusicPlayer(AbstractDemoPlayer): return self._cur_track + 1 @property - def repeat(self) -> str: + def repeat(self) -> RepeatMode: """Return current repeat mode.""" return self._repeat @@ -411,10 +368,10 @@ class DemoMusicPlayer(AbstractDemoPlayer): """Clear players playlist.""" self.tracks = [] self._cur_track = 0 - self._player_state = STATE_OFF + self._attr_state = MediaPlayerState.OFF self.schedule_update_ha_state() - def set_repeat(self, repeat: str) -> None: + def set_repeat(self, repeat: RepeatMode) -> None: """Enable/disable repeat mode.""" self._repeat = repeat self.schedule_update_ha_state() @@ -437,11 +394,11 @@ class DemoTVShowPlayer(AbstractDemoPlayer): # We only implement the methods that we support - _attr_device_class = MediaPlayerDeviceClass.TV + _attr_media_content_type = MediaType.TVSHOW def __init__(self) -> None: """Initialize the demo device.""" - super().__init__("Lounge room") + super().__init__("Lounge room", MediaPlayerDeviceClass.TV) self._cur_episode = 1 self._episode_count = 13 self._source = "dvd" @@ -452,11 +409,6 @@ class DemoTVShowPlayer(AbstractDemoPlayer): """Return the content ID of current playing media.""" return "house-of-cards-1" - @property - def media_content_type(self) -> str: - """Return the content type of current playing media.""" - return MEDIA_TYPE_TVSHOW - @property def media_duration(self) -> int: """Return the duration of current playing media in seconds.""" diff --git a/homeassistant/components/demo/stt.py b/homeassistant/components/demo/stt.py index c035021b5ee..9c3cf89d80e 100644 --- a/homeassistant/components/demo/stt.py +++ b/homeassistant/components/demo/stt.py @@ -3,13 +3,15 @@ from __future__ import annotations from aiohttp import StreamReader -from homeassistant.components.stt import Provider, SpeechMetadata, SpeechResult -from homeassistant.components.stt.const import ( +from homeassistant.components.stt import ( AudioBitRates, AudioChannels, AudioCodecs, AudioFormats, AudioSampleRates, + Provider, + SpeechMetadata, + SpeechResult, SpeechResultState, ) from homeassistant.core import HomeAssistant diff --git a/homeassistant/components/demo/translations/bg.json b/homeassistant/components/demo/translations/bg.json index 9609b0e64d9..2ecf8f371eb 100644 --- a/homeassistant/components/demo/translations/bg.json +++ b/homeassistant/components/demo/translations/bg.json @@ -1,3 +1,16 @@ { + "issues": { + "bad_psu": { + "fix_flow": { + "step": { + "confirm": { + "description": "\u041d\u0430\u0442\u0438\u0441\u043d\u0435\u0442\u0435 \u0418\u0417\u041f\u0420\u0410\u0429\u0410\u041d\u0415, \u0437\u0430 \u0434\u0430 \u043f\u043e\u0442\u0432\u044a\u0440\u0434\u0438\u0442\u0435, \u0447\u0435 \u0437\u0430\u0445\u0440\u0430\u043d\u0432\u0430\u0449\u0438\u044f\u0442 \u0431\u043b\u043e\u043a \u0435 \u0441\u043c\u0435\u043d\u0435\u043d", + "title": "\u0417\u0430\u0445\u0440\u0430\u043d\u0432\u0430\u043d\u0435\u0442\u043e \u0442\u0440\u044f\u0431\u0432\u0430 \u0434\u0430 \u0431\u044a\u0434\u0435 \u0441\u043c\u0435\u043d\u0435\u043d\u043e" + } + } + }, + "title": "\u0417\u0430\u0445\u0440\u0430\u043d\u0432\u0430\u043d\u0435\u0442\u043e \u043d\u0435 \u0435 \u0441\u0442\u0430\u0431\u0438\u043b\u043d\u043e" + } + }, "title": "\u0414\u0435\u043c\u043e\u043d\u0441\u0442\u0440\u0430\u0446\u0438\u044f" } \ No newline at end of file diff --git a/homeassistant/components/demo/translations/fr.json b/homeassistant/components/demo/translations/fr.json index a09a7a1fd2f..4fa207692f3 100644 --- a/homeassistant/components/demo/translations/fr.json +++ b/homeassistant/components/demo/translations/fr.json @@ -14,7 +14,7 @@ "fix_flow": { "step": { "confirm": { - "description": "Appuyez sur OK une fois le liquide de clignotant rempli", + "description": "Appuyez sur VALIDER une fois le liquide de clignotant rempli", "title": "Le liquide de clignotant doit \u00eatre rempli" } } diff --git a/homeassistant/components/demo/translations/hu.json b/homeassistant/components/demo/translations/hu.json index a5aa872fc5c..ece6bad6bfd 100644 --- a/homeassistant/components/demo/translations/hu.json +++ b/homeassistant/components/demo/translations/hu.json @@ -15,7 +15,7 @@ "fix_flow": { "step": { "confirm": { - "description": "Nyomja meg az OK gombot, ha a villog\u00f3 folyad\u00e9kot felt\u00f6lt\u00f6tt\u00e9k.", + "description": "Nyomja meg az OK gombot, ha a folyad\u00e9kot felt\u00f6lt\u00f6tt\u00e9k.", "title": "A villog\u00f3 folyad\u00e9kot fel kell t\u00f6lteni" } } diff --git a/homeassistant/components/demo/translations/ja.json b/homeassistant/components/demo/translations/ja.json index bd4d650de1c..1130653c893 100644 --- a/homeassistant/components/demo/translations/ja.json +++ b/homeassistant/components/demo/translations/ja.json @@ -15,7 +15,7 @@ "fix_flow": { "step": { "confirm": { - "description": "\u30d6\u30ea\u30f3\u30ab\u30fc\u6db2\u306e\u88dc\u5145\u304c\u5b8c\u4e86\u3057\u305f\u3089\u3001OK\u3092\u62bc\u3057\u3066\u304f\u3060\u3055\u3044", + "description": "\u30a6\u30a4\u30f3\u30ab\u30fc\u6db2\u304c\u88dc\u5145\u3055\u308c\u305f\u3089\u3001SUBMIT \u3092\u62bc\u3057\u307e\u3059", "title": "\u30d6\u30ea\u30f3\u30ab\u30fc\u6db2\u3092\u88dc\u5145\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059" } } diff --git a/homeassistant/components/demo/translations/nl.json b/homeassistant/components/demo/translations/nl.json index 37b23b2aaac..08aeec0fcd0 100644 --- a/homeassistant/components/demo/translations/nl.json +++ b/homeassistant/components/demo/translations/nl.json @@ -1,4 +1,16 @@ { + "issues": { + "bad_psu": { + "fix_flow": { + "step": { + "confirm": { + "title": "De voeding moet worden vervangen" + } + } + }, + "title": "De voeding is niet stabiel" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/demo/translations/pt.json b/homeassistant/components/demo/translations/pt.json index 7d9ee992b39..b0c39f31567 100644 --- a/homeassistant/components/demo/translations/pt.json +++ b/homeassistant/components/demo/translations/pt.json @@ -1,4 +1,17 @@ { + "issues": { + "bad_psu": { + "fix_flow": { + "step": { + "confirm": { + "description": "Pressione ENVIAR para confirmar que a fonte de alimenta\u00e7\u00e3o foi substitu\u00edda", + "title": "A fonte de alimenta\u00e7\u00e3o precisa ser substitu\u00edda" + } + } + }, + "title": "A fonte de alimenta\u00e7\u00e3o n\u00e3o \u00e9 est\u00e1vel" + } + }, "options": { "step": { "options_1": { diff --git a/homeassistant/components/demo/update.py b/homeassistant/components/demo/update.py index 648ad59bb55..15e67ffa0a8 100644 --- a/homeassistant/components/demo/update.py +++ b/homeassistant/components/demo/update.py @@ -4,8 +4,11 @@ from __future__ import annotations import asyncio from typing import Any -from homeassistant.components.update import UpdateDeviceClass, UpdateEntity -from homeassistant.components.update.const import UpdateEntityFeature +from homeassistant.components.update import ( + UpdateDeviceClass, + UpdateEntity, + UpdateEntityFeature, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import DEVICE_DEFAULT_NAME from homeassistant.core import HomeAssistant diff --git a/homeassistant/components/denon/media_player.py b/homeassistant/components/denon/media_player.py index 2dd7a29e17e..8c71dd46c3e 100644 --- a/homeassistant/components/denon/media_player.py +++ b/homeassistant/components/denon/media_player.py @@ -10,8 +10,9 @@ from homeassistant.components.media_player import ( PLATFORM_SCHEMA, MediaPlayerEntity, MediaPlayerEntityFeature, + MediaPlayerState, ) -from homeassistant.const import CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON +from homeassistant.const import CONF_HOST, CONF_NAME from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -214,12 +215,12 @@ class DenonDevice(MediaPlayerEntity): return self._name @property - def state(self): + def state(self) -> MediaPlayerState | None: """Return the state of the device.""" if self._pwstate == "PWSTANDBY": - return STATE_OFF + return MediaPlayerState.OFF if self._pwstate == "PWON": - return STATE_ON + return MediaPlayerState.ON return None diff --git a/homeassistant/components/denonavr/media_player.py b/homeassistant/components/denonavr/media_player.py index c06d5a939a3..cc0e0c06656 100644 --- a/homeassistant/components/denonavr/media_player.py +++ b/homeassistant/components/denonavr/media_player.py @@ -22,19 +22,11 @@ import voluptuous as vol from homeassistant.components.media_player import ( MediaPlayerEntity, MediaPlayerEntityFeature, -) -from homeassistant.components.media_player.const import ( - MEDIA_TYPE_CHANNEL, - MEDIA_TYPE_MUSIC, + MediaPlayerState, + MediaType, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - ATTR_COMMAND, - CONF_HOST, - CONF_MODEL, - STATE_PAUSED, - STATE_PLAYING, -) +from homeassistant.const import ATTR_COMMAND, CONF_HOST, CONF_MODEL from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.entity import DeviceInfo @@ -297,11 +289,11 @@ class DenonDevice(MediaPlayerEntity): return None @property - def media_content_type(self): + def media_content_type(self) -> MediaType: """Content type of current playing media.""" - if self._receiver.state in (STATE_PLAYING, STATE_PAUSED): - return MEDIA_TYPE_MUSIC - return MEDIA_TYPE_CHANNEL + if self._receiver.state in {MediaPlayerState.PLAYING, MediaPlayerState.PAUSED}: + return MediaType.MUSIC + return MediaType.CHANNEL @property def media_duration(self): diff --git a/homeassistant/components/denonavr/translations/bg.json b/homeassistant/components/denonavr/translations/bg.json index 4b4384d0bc9..d1ce0ce7e22 100644 --- a/homeassistant/components/denonavr/translations/bg.json +++ b/homeassistant/components/denonavr/translations/bg.json @@ -14,5 +14,14 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "show_all_sources": "\u041f\u043e\u043a\u0430\u0437\u0432\u0430\u043d\u0435 \u043d\u0430 \u0432\u0441\u0438\u0447\u043a\u0438 \u0438\u0437\u0442\u043e\u0447\u043d\u0438\u0446\u0438" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/denonavr/translations/pt.json b/homeassistant/components/denonavr/translations/pt.json index 4a00952aaa5..fa509296c77 100644 --- a/homeassistant/components/denonavr/translations/pt.json +++ b/homeassistant/components/denonavr/translations/pt.json @@ -7,12 +7,12 @@ "step": { "select": { "data": { - "select_host": "IP do receptor" + "select_host": "IP do Receptor" } }, "user": { "data": { - "host": "endere\u00e7o de IP" + "host": "Endere\u00e7o IP" } } } diff --git a/homeassistant/components/derivative/sensor.py b/homeassistant/components/derivative/sensor.py index 8e1934dcecf..59e661fce0b 100644 --- a/homeassistant/components/derivative/sensor.py +++ b/homeassistant/components/derivative/sensor.py @@ -35,8 +35,6 @@ from .const import ( CONF_UNIT_TIME, ) -# mypy: allow-untyped-defs, no-check-untyped-defs - _LOGGER = logging.getLogger(__name__) ATTR_SOURCE_ID = "source" diff --git a/homeassistant/components/derivative/translations/ja.json b/homeassistant/components/derivative/translations/ja.json index c5939e95deb..e89ba315ccd 100644 --- a/homeassistant/components/derivative/translations/ja.json +++ b/homeassistant/components/derivative/translations/ja.json @@ -11,12 +11,12 @@ "unit_time": "\u6642\u9593\u5358\u4f4d" }, "data_description": { - "round": "\u51fa\u529b\u5024\u306e\u5c0f\u6570\u70b9\u4ee5\u4e0b\u306e\u6841\u6570\u3002", + "round": "\u51fa\u529b\u306e 10 \u9032\u6570\u306e\u6841\u6570\u3092\u5236\u5fa1\u3057\u307e\u3059\u3002", "time_window": "\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u308b\u5834\u5408\u3001\u30bb\u30f3\u30b5\u30fc\u306e\u5024\u306f\u3053\u306e\u30a6\u30a3\u30f3\u30c9\u30a6\u5185\u306e\u5fae\u5206\u306e\u6642\u9593\u52a0\u91cd\u79fb\u52d5\u5e73\u5747\u3068\u306a\u308a\u307e\u3059\u3002", - "unit_prefix": "\u5fae\u5206\u306f\u3001\u9078\u629e\u3055\u308c\u305f\u5358\u4f4d\u306e\u30d7\u30ec\u30d5\u30a3\u30c3\u30af\u30b9\u3068\u5fae\u5206\u306e\u6642\u9593\u5358\u4f4d\u306b\u5f93\u3063\u3066\u30b9\u30b1\u30fc\u30ea\u30f3\u30b0\u3055\u308c\u307e\u3059\u3002" + "unit_prefix": "\u51fa\u529b\u306f\u3001\u9078\u629e\u3057\u305f\u30e1\u30c8\u30ea\u30c3\u30af \u30d7\u30ec\u30d5\u30a3\u30c3\u30af\u30b9\u3068\u5c0e\u95a2\u6570\u306e\u6642\u9593\u5358\u4f4d\u306b\u5f93\u3063\u3066\u30b9\u30b1\u30fc\u30ea\u30f3\u30b0\u3055\u308c\u307e\u3059\u3002" }, - "description": "\u7cbe\u5ea6\u306f\u3001\u51fa\u529b\u306e\u5c0f\u6570\u70b9\u4ee5\u4e0b\u306e\u6841\u6570\u3092\u5236\u5fa1\u3057\u307e\u3059\u3002\n\u6642\u9593\u7a93\u304c0\u3067\u306a\u3044\u5834\u5408\u3001\u30bb\u30f3\u30b5\u30fc\u306e\u5024\u306f\u7a93\u5185\u306e\u5fae\u5206\u306e\u6642\u9593\u52a0\u91cd\u79fb\u52d5\u5e73\u5747\u306b\u306a\u308a\u307e\u3059\u3002\n\u5fae\u5206\u306f\u3001\u9078\u629e\u3055\u308c\u305f\u5358\u4f4d\u306e\u30d7\u30ea\u30d5\u30a3\u30c3\u30af\u30b9\u3068\u5fae\u5206\u306e\u6642\u9593\u5358\u4f4d\u306b\u5f93\u3063\u3066\u30b9\u30b1\u30fc\u30ea\u30f3\u30b0\u3055\u308c\u307e\u3059\u3002", - "title": "\u65b0\u3057\u3044\u6d3e\u751f(Derivative)\u30bb\u30f3\u30b5\u30fc" + "description": "\u30bb\u30f3\u30b5\u30fc\u306e\u5c0e\u95a2\u6570\u3092\u63a8\u5b9a\u3059\u308b\u30bb\u30f3\u30b5\u30fc\u3092\u4f5c\u6210\u3057\u307e\u3059\u3002", + "title": "\u5fae\u5206\u30bb\u30f3\u30b5\u30fc\u3092\u8ffd\u52a0" } } }, @@ -32,9 +32,9 @@ "unit_time": "\u6642\u9593\u5358\u4f4d" }, "data_description": { - "round": "\u51fa\u529b\u5024\u306e\u5c0f\u6570\u70b9\u4ee5\u4e0b\u306e\u6841\u6570\u3002", + "round": "\u51fa\u529b\u306e 10 \u9032\u6570\u306e\u6841\u6570\u3092\u5236\u5fa1\u3057\u307e\u3059\u3002", "time_window": "\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u308b\u5834\u5408\u3001\u30bb\u30f3\u30b5\u30fc\u306e\u5024\u306f\u3053\u306e\u30a6\u30a3\u30f3\u30c9\u30a6\u5185\u306e\u5fae\u5206\u306e\u6642\u9593\u52a0\u91cd\u79fb\u52d5\u5e73\u5747\u3068\u306a\u308a\u307e\u3059\u3002", - "unit_prefix": "\u5fae\u5206\u306f\u3001\u9078\u629e\u3055\u308c\u305f\u5358\u4f4d\u306e\u30d7\u30ec\u30d5\u30a3\u30c3\u30af\u30b9\u3068\u5fae\u5206\u306e\u6642\u9593\u5358\u4f4d\u306b\u5f93\u3063\u3066\u30b9\u30b1\u30fc\u30ea\u30f3\u30b0\u3055\u308c\u307e\u3059\u3002." + "unit_prefix": "\u51fa\u529b\u306f\u3001\u9078\u629e\u3057\u305f\u30e1\u30c8\u30ea\u30c3\u30af \u30d7\u30ec\u30d5\u30a3\u30c3\u30af\u30b9\u3068\u5c0e\u95a2\u6570\u306e\u6642\u9593\u5358\u4f4d\u306b\u5f93\u3063\u3066\u30b9\u30b1\u30fc\u30ea\u30f3\u30b0\u3055\u308c\u307e\u3059\u3002." } } } diff --git a/homeassistant/components/derivative/translations/pt.json b/homeassistant/components/derivative/translations/pt.json index 0b3ad11d873..95b042df331 100644 --- a/homeassistant/components/derivative/translations/pt.json +++ b/homeassistant/components/derivative/translations/pt.json @@ -14,6 +14,9 @@ "options": { "step": { "init": { + "data": { + "round": "Precis\u00e3o" + }, "data_description": { "unit_prefix": "." } diff --git a/homeassistant/components/deutsche_bahn/translations/bg.json b/homeassistant/components/deutsche_bahn/translations/bg.json new file mode 100644 index 00000000000..84d8d17f23a --- /dev/null +++ b/homeassistant/components/deutsche_bahn/translations/bg.json @@ -0,0 +1,7 @@ +{ + "issues": { + "pending_removal": { + "title": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u043d\u0430 Deutsche Bahn \u0441\u0435 \u043f\u0440\u0435\u043c\u0430\u0445\u0432\u0430" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deutsche_bahn/translations/nl.json b/homeassistant/components/deutsche_bahn/translations/nl.json new file mode 100644 index 00000000000..6aabd5b3a6d --- /dev/null +++ b/homeassistant/components/deutsche_bahn/translations/nl.json @@ -0,0 +1,7 @@ +{ + "issues": { + "pending_removal": { + "title": "De Deutsche Bahn-integratie wordt verwijderd" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/device_automation/__init__.py b/homeassistant/components/device_automation/__init__.py index 93119d1b4a0..3b75f4fff4c 100644 --- a/homeassistant/components/device_automation/__init__.py +++ b/homeassistant/components/device_automation/__init__.py @@ -30,6 +30,12 @@ from homeassistant.helpers.typing import ConfigType from homeassistant.loader import IntegrationNotFound from homeassistant.requirements import async_get_integration_with_requirements +from .const import ( # noqa: F401 + CONF_IS_OFF, + CONF_IS_ON, + CONF_TURNED_OFF, + CONF_TURNED_ON, +) from .exceptions import DeviceNotFound, InvalidDeviceAutomationConfig if TYPE_CHECKING: diff --git a/homeassistant/components/device_automation/action.py b/homeassistant/components/device_automation/action.py index 081b6bb283a..432ff2fdb7d 100644 --- a/homeassistant/components/device_automation/action.py +++ b/homeassistant/components/device_automation/action.py @@ -7,6 +7,7 @@ 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 @@ -51,14 +52,15 @@ async def async_validate_action_config( ) -> ConfigType: """Validate config.""" try: + config = cv.DEVICE_ACTION_SCHEMA(config) 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 + except (vol.Invalid, InvalidDeviceAutomationConfig) as err: + raise vol.Invalid("invalid action configuration: " + str(err)) from err 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..3b0a5263f9e 100644 --- a/homeassistant/components/device_automation/condition.py +++ b/homeassistant/components/device_automation/condition.py @@ -58,8 +58,8 @@ async def async_validate_condition_config( 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 + except (vol.Invalid, InvalidDeviceAutomationConfig) as err: + raise vol.Invalid("invalid condition configuration: " + str(err)) from err async def async_condition_from_config( diff --git a/homeassistant/components/device_automation/entity.py b/homeassistant/components/device_automation/entity.py index c7716f38712..f38daf2dae6 100644 --- a/homeassistant/components/device_automation/entity.py +++ b/homeassistant/components/device_automation/entity.py @@ -13,8 +13,6 @@ from homeassistant.helpers.typing import ConfigType from . import DEVICE_TRIGGER_BASE_SCHEMA from .const import CONF_CHANGED_STATES -# mypy: allow-untyped-calls, allow-untyped-defs - ENTITY_TRIGGERS = [ { # Trigger when entity is turned on or off diff --git a/homeassistant/components/device_automation/manifest.json b/homeassistant/components/device_automation/manifest.json index 033a54312be..e897cb5a29f 100644 --- a/homeassistant/components/device_automation/manifest.json +++ b/homeassistant/components/device_automation/manifest.json @@ -3,5 +3,6 @@ "name": "Device Automation", "documentation": "https://www.home-assistant.io/integrations/device_automation", "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "system" } diff --git a/homeassistant/components/device_automation/toggle_entity.py b/homeassistant/components/device_automation/toggle_entity.py index f14c4de5c2f..e5061cb691e 100644 --- a/homeassistant/components/device_automation/toggle_entity.py +++ b/homeassistant/components/device_automation/toggle_entity.py @@ -33,8 +33,6 @@ from .const import ( CONF_TURNED_ON, ) -# mypy: allow-untyped-calls, allow-untyped-defs - ENTITY_ACTIONS = [ { # Turn entity off diff --git a/homeassistant/components/device_automation/trigger.py b/homeassistant/components/device_automation/trigger.py index bd72b24d844..aac56b39846 100644 --- a/homeassistant/components/device_automation/trigger.py +++ b/homeassistant/components/device_automation/trigger.py @@ -58,14 +58,15 @@ async def async_validate_trigger_config( ) -> ConfigType: """Validate config.""" try: + config = TRIGGER_SCHEMA(config) platform = await async_get_device_automation_platform( hass, config[CONF_DOMAIN], DeviceAutomationType.TRIGGER ) if not hasattr(platform, "async_validate_trigger_config"): return cast(ConfigType, platform.TRIGGER_SCHEMA(config)) return await platform.async_validate_trigger_config(hass, config) - except InvalidDeviceAutomationConfig as err: - raise vol.Invalid(str(err) or "Invalid trigger configuration") from err + except (vol.Invalid, InvalidDeviceAutomationConfig) as err: + raise InvalidDeviceAutomationConfig("invalid trigger configuration") from err async def async_attach_trigger( diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index 9e58c5bbc92..2d3353a1110 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -13,6 +13,7 @@ from .const import ( # noqa: F401 ATTR_DEV_ID, ATTR_GPS, ATTR_HOST_NAME, + ATTR_IP, ATTR_LOCATION_NAME, ATTR_MAC, ATTR_SOURCE_TYPE, @@ -20,8 +21,12 @@ from .const import ( # noqa: F401 CONF_NEW_DEVICE_DEFAULTS, CONF_SCAN_INTERVAL, CONF_TRACK_NEW, + CONNECTED_DEVICE_REGISTERED, + DEFAULT_CONSIDER_HOME, + DEFAULT_TRACK_NEW, DOMAIN, ENTITY_ID_FORMAT, + SCAN_INTERVAL, SOURCE_TYPE_BLUETOOTH, SOURCE_TYPE_BLUETOOTH_LE, SOURCE_TYPE_GPS, diff --git a/homeassistant/components/device_tracker/config_entry.py b/homeassistant/components/device_tracker/config_entry.py index b587f17d58e..64ad55aee37 100644 --- a/homeassistant/components/device_tracker/config_entry.py +++ b/homeassistant/components/device_tracker/config_entry.py @@ -33,15 +33,19 @@ from .const import ( SourceType, ) +# mypy: disallow-any-generics + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up an entry.""" - component: EntityComponent | None = hass.data.get(DOMAIN) + component: EntityComponent[BaseTrackerEntity] | None = hass.data.get(DOMAIN) if component is not None: return await component.async_setup_entry(entry) - component = hass.data[DOMAIN] = EntityComponent(LOGGER, DOMAIN, hass) + component = hass.data[DOMAIN] = EntityComponent[BaseTrackerEntity]( + LOGGER, DOMAIN, hass + ) # Clean up old devices created by device tracker entities in the past. # Can be removed after 2022.6 @@ -70,7 +74,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload an entry.""" - component: EntityComponent = hass.data[DOMAIN] + component: EntityComponent[BaseTrackerEntity] = hass.data[DOMAIN] return await component.async_unload_entry(entry) diff --git a/homeassistant/components/device_tracker/legacy.py b/homeassistant/components/device_tracker/legacy.py index 8216c5fba27..09fd5dce432 100644 --- a/homeassistant/components/device_tracker/legacy.py +++ b/homeassistant/components/device_tracker/legacy.py @@ -71,12 +71,7 @@ from .const import ( SERVICE_SEE: Final = "see" -SOURCE_TYPES: Final[tuple[str, ...]] = ( - SourceType.GPS, - SourceType.ROUTER, - SourceType.BLUETOOTH, - SourceType.BLUETOOTH_LE, -) +SOURCE_TYPES = [cls.value for cls in SourceType] NEW_DEVICE_DEFAULTS_SCHEMA = vol.Any( None, @@ -108,7 +103,7 @@ SERVICE_SEE_PAYLOAD_SCHEMA: Final[vol.Schema] = vol.Schema( ATTR_GPS_ACCURACY: cv.positive_int, ATTR_BATTERY: cv.positive_int, ATTR_ATTRIBUTES: dict, - ATTR_SOURCE_TYPE: vol.In(SOURCE_TYPES), + ATTR_SOURCE_TYPE: vol.Coerce(SourceType), ATTR_CONSIDER_HOME: cv.time_period, # Temp workaround for iOS app introduced in 0.65 vol.Optional("battery_status"): str, diff --git a/homeassistant/components/device_tracker/manifest.json b/homeassistant/components/device_tracker/manifest.json index 7abd68b03e2..1ce4349e537 100644 --- a/homeassistant/components/device_tracker/manifest.json +++ b/homeassistant/components/device_tracker/manifest.json @@ -5,5 +5,6 @@ "dependencies": ["zone"], "after_dependencies": [], "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "entity" } diff --git a/homeassistant/components/devolo_home_control/climate.py b/homeassistant/components/devolo_home_control/climate.py index 4b8e8fc00e6..95e0628d534 100644 --- a/homeassistant/components/devolo_home_control/climate.py +++ b/homeassistant/components/devolo_home_control/climate.py @@ -10,8 +10,9 @@ from homeassistant.components.climate import ( ATTR_TEMPERATURE, TEMP_CELSIUS, ClimateEntity, + ClimateEntityFeature, + HVACMode, ) -from homeassistant.components.climate.const import ClimateEntityFeature, HVACMode from homeassistant.config_entries import ConfigEntry from homeassistant.const import PRECISION_HALVES, PRECISION_TENTHS from homeassistant.core import HomeAssistant diff --git a/homeassistant/components/devolo_home_control/translations/es.json b/homeassistant/components/devolo_home_control/translations/es.json index 91a06530273..b8d998915fb 100644 --- a/homeassistant/components/devolo_home_control/translations/es.json +++ b/homeassistant/components/devolo_home_control/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "error": { "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", diff --git a/homeassistant/components/devolo_home_control/translations/pt.json b/homeassistant/components/devolo_home_control/translations/pt.json index d60cc81f541..f8e1acc359d 100644 --- a/homeassistant/components/devolo_home_control/translations/pt.json +++ b/homeassistant/components/devolo_home_control/translations/pt.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Conta j\u00e1 configurada" + "already_configured": "Conta j\u00e1 configurada", + "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida" }, "error": { "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" @@ -16,6 +17,7 @@ }, "zeroconf_confirm": { "data": { + "mydevolo_url": "mydevolo [VOID]", "password": "Palavra-passe" } } diff --git a/homeassistant/components/devolo_home_network/translations/cs.json b/homeassistant/components/devolo_home_network/translations/cs.json index e1bf8e7f45f..04f18366eaf 100644 --- a/homeassistant/components/devolo_home_network/translations/cs.json +++ b/homeassistant/components/devolo_home_network/translations/cs.json @@ -1,7 +1,18 @@ { "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + }, "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "ip_address": "IP adresa" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/devolo_home_network/translations/pt.json b/homeassistant/components/devolo_home_network/translations/pt.json new file mode 100644 index 00000000000..fb6f8840412 --- /dev/null +++ b/homeassistant/components/devolo_home_network/translations/pt.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "unknown": "Erro inesperado" + }, + "flow_title": "{product} ({name})", + "step": { + "user": { + "data": { + "ip_address": "Endere\u00e7o IP" + }, + "description": "Quer dar inicio \u00e0 configura\u00e7\u00e3o?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dhcp/__init__.py b/homeassistant/components/dhcp/__init__.py index 7a5854fc53e..ab8787f4bff 100644 --- a/homeassistant/components/dhcp/__init__.py +++ b/homeassistant/components/dhcp/__init__.py @@ -26,7 +26,7 @@ from scapy.config import conf from scapy.error import Scapy_Exception from homeassistant import config_entries -from homeassistant.components.device_tracker.const import ( +from homeassistant.components.device_tracker import ( ATTR_HOST_NAME, ATTR_IP, ATTR_MAC, diff --git a/homeassistant/components/dhcp/manifest.json b/homeassistant/components/dhcp/manifest.json index f3f44f6dc9b..2ebb0fd63e0 100644 --- a/homeassistant/components/dhcp/manifest.json +++ b/homeassistant/components/dhcp/manifest.json @@ -6,5 +6,6 @@ "codeowners": ["@bdraco"], "quality_scale": "internal", "iot_class": "local_push", - "loggers": ["aiodiscover", "dnspython", "pyroute2", "scapy"] + "loggers": ["aiodiscover", "dnspython", "pyroute2", "scapy"], + "integration_type": "system" } diff --git a/homeassistant/components/diagnostics/manifest.json b/homeassistant/components/diagnostics/manifest.json index ad6edf110b0..383ebebd947 100644 --- a/homeassistant/components/diagnostics/manifest.json +++ b/homeassistant/components/diagnostics/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/diagnostics", "dependencies": ["http"], "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "system" } diff --git a/homeassistant/components/dialogflow/translations/pt.json b/homeassistant/components/dialogflow/translations/pt.json index 56c91431f13..1c1db64bf45 100644 --- a/homeassistant/components/dialogflow/translations/pt.json +++ b/homeassistant/components/dialogflow/translations/pt.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "N\u00e3o ligado ao Home Assistant Cloud.", "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel.", "webhook_not_internet_accessible": "O seu Home Assistant necessita estar acess\u00edvel a partir da Internet para receber mensagens do tipo webhook." }, diff --git a/homeassistant/components/directv/media_player.py b/homeassistant/components/directv/media_player.py index 7d1434e9909..bc838757854 100644 --- a/homeassistant/components/directv/media_player.py +++ b/homeassistant/components/directv/media_player.py @@ -10,15 +10,10 @@ from homeassistant.components.media_player import ( MediaPlayerDeviceClass, MediaPlayerEntity, MediaPlayerEntityFeature, -) -from homeassistant.components.media_player.const import ( - MEDIA_TYPE_CHANNEL, - MEDIA_TYPE_MOVIE, - MEDIA_TYPE_MUSIC, - MEDIA_TYPE_TVSHOW, + MediaPlayerState, + MediaType, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import STATE_OFF, STATE_PAUSED, STATE_PLAYING from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import dt as dt_util @@ -34,7 +29,7 @@ from .entity import DIRECTVEntity _LOGGER = logging.getLogger(__name__) -KNOWN_MEDIA_TYPES = [MEDIA_TYPE_MOVIE, MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW] +KNOWN_MEDIA_TYPES = {MediaType.MOVIE, MediaType.MUSIC, MediaType.TVSHOW} SUPPORT_DTV = ( MediaPlayerEntityFeature.PAUSE @@ -134,18 +129,18 @@ class DIRECTVMediaPlayer(DIRECTVEntity, MediaPlayerEntity): # MediaPlayerEntity properties and methods @property - def state(self): + def state(self) -> MediaPlayerState: """Return the state of the device.""" if self._is_standby: - return STATE_OFF + return MediaPlayerState.OFF # For recorded media we can determine if it is paused or not. # For live media we're unable to determine and will always return # playing instead. if self._paused: - return STATE_PAUSED + return MediaPlayerState.PAUSED - return STATE_PLAYING + return MediaPlayerState.PLAYING @property def media_content_id(self): @@ -156,7 +151,7 @@ class DIRECTVMediaPlayer(DIRECTVEntity, MediaPlayerEntity): return self._program.program_id @property - def media_content_type(self): + def media_content_type(self) -> MediaType | None: """Return the content type of current playing media.""" if self._is_standby or self._program is None: return None @@ -164,7 +159,7 @@ class DIRECTVMediaPlayer(DIRECTVEntity, MediaPlayerEntity): if self._program.program_type in KNOWN_MEDIA_TYPES: return self._program.program_type - return MEDIA_TYPE_MOVIE + return MediaType.MOVIE @property def media_duration(self): @@ -196,7 +191,7 @@ class DIRECTVMediaPlayer(DIRECTVEntity, MediaPlayerEntity): if self._is_standby or self._program is None: return None - if self.media_content_type == MEDIA_TYPE_MUSIC: + if self.media_content_type == MediaType.MUSIC: return self._program.music_title return self._program.title @@ -320,14 +315,14 @@ class DIRECTVMediaPlayer(DIRECTVEntity, MediaPlayerEntity): await self.dtv.remote("ffwd", self._address) 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: """Select input source.""" - if media_type != MEDIA_TYPE_CHANNEL: + if media_type != MediaType.CHANNEL: _LOGGER.error( "Invalid media type %s. Only %s is supported", media_type, - MEDIA_TYPE_CHANNEL, + MediaType.CHANNEL, ) return diff --git a/homeassistant/components/discord/translations/cs.json b/homeassistant/components/discord/translations/cs.json new file mode 100644 index 00000000000..45e02001105 --- /dev/null +++ b/homeassistant/components/discord/translations/cs.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/discord/translations/es.json b/homeassistant/components/discord/translations/es.json index df4bc5a7fa3..0ce9ee06583 100644 --- a/homeassistant/components/discord/translations/es.json +++ b/homeassistant/components/discord/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "El servicio ya est\u00e1 configurado", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "error": { "cannot_connect": "No se pudo conectar", diff --git a/homeassistant/components/discord/translations/pt.json b/homeassistant/components/discord/translations/pt.json index 9925c2a7416..2c961f1708a 100644 --- a/homeassistant/components/discord/translations/pt.json +++ b/homeassistant/components/discord/translations/pt.json @@ -1,22 +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": "Reautentica\u00e7\u00e3o bem sucedida" }, "error": { "cannot_connect": "Falha na liga\u00e7\u00e3o", - "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" }, "step": { "reauth_confirm": { "data": { "api_token": "API Token" - } + }, + "description": "Consulte a documenta\u00e7\u00e3o sobre como obter a sua chave de bot do Discord. \n\n{url}" }, "user": { "data": { "api_token": "API Token" - } + }, + "description": "Consulte a documenta\u00e7\u00e3o sobre como obter a sua chave de bot do Discord. \n\n{url}" } } } diff --git a/homeassistant/components/discovery/manifest.json b/homeassistant/components/discovery/manifest.json index 6f97993c788..c98cdfa60a6 100644 --- a/homeassistant/components/discovery/manifest.json +++ b/homeassistant/components/discovery/manifest.json @@ -6,5 +6,6 @@ "after_dependencies": ["zeroconf"], "codeowners": ["@home-assistant/core"], "quality_scale": "internal", - "loggers": ["netdisco"] + "loggers": ["netdisco"], + "integration_type": "system" } diff --git a/homeassistant/components/dlna_dmr/const.py b/homeassistant/components/dlna_dmr/const.py index 4f9982061fb..4cea664f058 100644 --- a/homeassistant/components/dlna_dmr/const.py +++ b/homeassistant/components/dlna_dmr/const.py @@ -7,7 +7,7 @@ from typing import Final from async_upnp_client.profiles.dlna import PlayMode as _PlayMode -from homeassistant.components.media_player import const as _mp_const +from homeassistant.components.media_player import MediaType, RepeatMode LOGGER = logging.getLogger(__package__) @@ -28,66 +28,66 @@ PROTOCOL_ANY: Final = "*" STREAMABLE_PROTOCOLS: Final = [PROTOCOL_HTTP, PROTOCOL_RTSP, PROTOCOL_ANY] # Map UPnP class to media_player media_content_type -MEDIA_TYPE_MAP: Mapping[str, str] = { - "object": _mp_const.MEDIA_TYPE_URL, - "object.item": _mp_const.MEDIA_TYPE_URL, - "object.item.imageItem": _mp_const.MEDIA_TYPE_IMAGE, - "object.item.imageItem.photo": _mp_const.MEDIA_TYPE_IMAGE, - "object.item.audioItem": _mp_const.MEDIA_TYPE_MUSIC, - "object.item.audioItem.musicTrack": _mp_const.MEDIA_TYPE_MUSIC, - "object.item.audioItem.audioBroadcast": _mp_const.MEDIA_TYPE_MUSIC, - "object.item.audioItem.audioBook": _mp_const.MEDIA_TYPE_PODCAST, - "object.item.videoItem": _mp_const.MEDIA_TYPE_VIDEO, - "object.item.videoItem.movie": _mp_const.MEDIA_TYPE_MOVIE, - "object.item.videoItem.videoBroadcast": _mp_const.MEDIA_TYPE_TVSHOW, - "object.item.videoItem.musicVideoClip": _mp_const.MEDIA_TYPE_VIDEO, - "object.item.playlistItem": _mp_const.MEDIA_TYPE_PLAYLIST, - "object.item.textItem": _mp_const.MEDIA_TYPE_URL, - "object.item.bookmarkItem": _mp_const.MEDIA_TYPE_URL, - "object.item.epgItem": _mp_const.MEDIA_TYPE_EPISODE, - "object.item.epgItem.audioProgram": _mp_const.MEDIA_TYPE_EPISODE, - "object.item.epgItem.videoProgram": _mp_const.MEDIA_TYPE_EPISODE, - "object.container": _mp_const.MEDIA_TYPE_PLAYLIST, - "object.container.person": _mp_const.MEDIA_TYPE_ARTIST, - "object.container.person.musicArtist": _mp_const.MEDIA_TYPE_ARTIST, - "object.container.playlistContainer": _mp_const.MEDIA_TYPE_PLAYLIST, - "object.container.album": _mp_const.MEDIA_TYPE_ALBUM, - "object.container.album.musicAlbum": _mp_const.MEDIA_TYPE_ALBUM, - "object.container.album.photoAlbum": _mp_const.MEDIA_TYPE_ALBUM, - "object.container.genre": _mp_const.MEDIA_TYPE_GENRE, - "object.container.genre.musicGenre": _mp_const.MEDIA_TYPE_GENRE, - "object.container.genre.movieGenre": _mp_const.MEDIA_TYPE_GENRE, - "object.container.channelGroup": _mp_const.MEDIA_TYPE_CHANNELS, - "object.container.channelGroup.audioChannelGroup": _mp_const.MEDIA_TYPE_CHANNELS, - "object.container.channelGroup.videoChannelGroup": _mp_const.MEDIA_TYPE_CHANNELS, - "object.container.epgContainer": _mp_const.MEDIA_TYPE_TVSHOW, - "object.container.storageSystem": _mp_const.MEDIA_TYPE_PLAYLIST, - "object.container.storageVolume": _mp_const.MEDIA_TYPE_PLAYLIST, - "object.container.storageFolder": _mp_const.MEDIA_TYPE_PLAYLIST, - "object.container.bookmarkFolder": _mp_const.MEDIA_TYPE_PLAYLIST, +MEDIA_TYPE_MAP: Mapping[str, MediaType] = { + "object": MediaType.URL, + "object.item": MediaType.URL, + "object.item.imageItem": MediaType.IMAGE, + "object.item.imageItem.photo": MediaType.IMAGE, + "object.item.audioItem": MediaType.MUSIC, + "object.item.audioItem.musicTrack": MediaType.MUSIC, + "object.item.audioItem.audioBroadcast": MediaType.MUSIC, + "object.item.audioItem.audioBook": MediaType.PODCAST, + "object.item.videoItem": MediaType.VIDEO, + "object.item.videoItem.movie": MediaType.MOVIE, + "object.item.videoItem.videoBroadcast": MediaType.TVSHOW, + "object.item.videoItem.musicVideoClip": MediaType.VIDEO, + "object.item.playlistItem": MediaType.PLAYLIST, + "object.item.textItem": MediaType.URL, + "object.item.bookmarkItem": MediaType.URL, + "object.item.epgItem": MediaType.EPISODE, + "object.item.epgItem.audioProgram": MediaType.EPISODE, + "object.item.epgItem.videoProgram": MediaType.EPISODE, + "object.container": MediaType.PLAYLIST, + "object.container.person": MediaType.ARTIST, + "object.container.person.musicArtist": MediaType.ARTIST, + "object.container.playlistContainer": MediaType.PLAYLIST, + "object.container.album": MediaType.ALBUM, + "object.container.album.musicAlbum": MediaType.ALBUM, + "object.container.album.photoAlbum": MediaType.ALBUM, + "object.container.genre": MediaType.GENRE, + "object.container.genre.musicGenre": MediaType.GENRE, + "object.container.genre.movieGenre": MediaType.GENRE, + "object.container.channelGroup": MediaType.CHANNELS, + "object.container.channelGroup.audioChannelGroup": MediaType.CHANNELS, + "object.container.channelGroup.videoChannelGroup": MediaType.CHANNELS, + "object.container.epgContainer": MediaType.TVSHOW, + "object.container.storageSystem": MediaType.PLAYLIST, + "object.container.storageVolume": MediaType.PLAYLIST, + "object.container.storageFolder": MediaType.PLAYLIST, + "object.container.bookmarkFolder": MediaType.PLAYLIST, } # Map media_player media_content_type to UPnP class. Not everything will map # directly, in which case it's not specified and other defaults will be used. -MEDIA_UPNP_CLASS_MAP: Mapping[str, str] = { - _mp_const.MEDIA_TYPE_ALBUM: "object.container.album.musicAlbum", - _mp_const.MEDIA_TYPE_ARTIST: "object.container.person.musicArtist", - _mp_const.MEDIA_TYPE_CHANNEL: "object.item.videoItem.videoBroadcast", - _mp_const.MEDIA_TYPE_CHANNELS: "object.container.channelGroup", - _mp_const.MEDIA_TYPE_COMPOSER: "object.container.person.musicArtist", - _mp_const.MEDIA_TYPE_CONTRIBUTING_ARTIST: "object.container.person.musicArtist", - _mp_const.MEDIA_TYPE_EPISODE: "object.item.epgItem.videoProgram", - _mp_const.MEDIA_TYPE_GENRE: "object.container.genre", - _mp_const.MEDIA_TYPE_IMAGE: "object.item.imageItem", - _mp_const.MEDIA_TYPE_MOVIE: "object.item.videoItem.movie", - _mp_const.MEDIA_TYPE_MUSIC: "object.item.audioItem.musicTrack", - _mp_const.MEDIA_TYPE_PLAYLIST: "object.item.playlistItem", - _mp_const.MEDIA_TYPE_PODCAST: "object.item.audioItem.audioBook", - _mp_const.MEDIA_TYPE_SEASON: "object.item.epgItem.videoProgram", - _mp_const.MEDIA_TYPE_TRACK: "object.item.audioItem.musicTrack", - _mp_const.MEDIA_TYPE_TVSHOW: "object.item.videoItem.videoBroadcast", - _mp_const.MEDIA_TYPE_URL: "object.item.bookmarkItem", - _mp_const.MEDIA_TYPE_VIDEO: "object.item.videoItem", +MEDIA_UPNP_CLASS_MAP: Mapping[MediaType | str, str] = { + MediaType.ALBUM: "object.container.album.musicAlbum", + MediaType.ARTIST: "object.container.person.musicArtist", + MediaType.CHANNEL: "object.item.videoItem.videoBroadcast", + MediaType.CHANNELS: "object.container.channelGroup", + MediaType.COMPOSER: "object.container.person.musicArtist", + MediaType.CONTRIBUTING_ARTIST: "object.container.person.musicArtist", + MediaType.EPISODE: "object.item.epgItem.videoProgram", + MediaType.GENRE: "object.container.genre", + MediaType.IMAGE: "object.item.imageItem", + MediaType.MOVIE: "object.item.videoItem.movie", + MediaType.MUSIC: "object.item.audioItem.musicTrack", + MediaType.PLAYLIST: "object.item.playlistItem", + MediaType.PODCAST: "object.item.audioItem.audioBook", + MediaType.SEASON: "object.item.epgItem.videoProgram", + MediaType.TRACK: "object.item.audioItem.musicTrack", + MediaType.TVSHOW: "object.item.videoItem.videoBroadcast", + MediaType.URL: "object.item.bookmarkItem", + MediaType.VIDEO: "object.item.videoItem", } # Translation of MediaMetadata keys to DIDL-Lite keys. @@ -109,32 +109,32 @@ MEDIA_METADATA_DIDL: Mapping[str, str] = { # of play modes in order of suitability. Fall back to _PlayMode.NORMAL in any # case. NOTE: This list is slightly different to that in SHUFFLE_PLAY_MODES, # due to fallback behaviour when turning on repeat modes. -REPEAT_PLAY_MODES: Mapping[tuple[bool, str], list[_PlayMode]] = { - (False, _mp_const.REPEAT_MODE_OFF): [ +REPEAT_PLAY_MODES: Mapping[tuple[bool, RepeatMode], list[_PlayMode]] = { + (False, RepeatMode.OFF): [ _PlayMode.NORMAL, ], - (False, _mp_const.REPEAT_MODE_ONE): [ + (False, RepeatMode.ONE): [ _PlayMode.REPEAT_ONE, _PlayMode.REPEAT_ALL, _PlayMode.NORMAL, ], - (False, _mp_const.REPEAT_MODE_ALL): [ + (False, RepeatMode.ALL): [ _PlayMode.REPEAT_ALL, _PlayMode.REPEAT_ONE, _PlayMode.NORMAL, ], - (True, _mp_const.REPEAT_MODE_OFF): [ + (True, RepeatMode.OFF): [ _PlayMode.SHUFFLE, _PlayMode.RANDOM, _PlayMode.NORMAL, ], - (True, _mp_const.REPEAT_MODE_ONE): [ + (True, RepeatMode.ONE): [ _PlayMode.REPEAT_ONE, _PlayMode.RANDOM, _PlayMode.SHUFFLE, _PlayMode.NORMAL, ], - (True, _mp_const.REPEAT_MODE_ALL): [ + (True, RepeatMode.ALL): [ _PlayMode.RANDOM, _PlayMode.REPEAT_ALL, _PlayMode.SHUFFLE, @@ -146,31 +146,31 @@ REPEAT_PLAY_MODES: Mapping[tuple[bool, str], list[_PlayMode]] = { # of play modes in order of suitability. Fall back to _PlayMode.NORMAL in any # case. SHUFFLE_PLAY_MODES: Mapping[tuple[bool, str], list[_PlayMode]] = { - (False, _mp_const.REPEAT_MODE_OFF): [ + (False, RepeatMode.OFF): [ _PlayMode.NORMAL, ], - (False, _mp_const.REPEAT_MODE_ONE): [ + (False, RepeatMode.ONE): [ _PlayMode.REPEAT_ONE, _PlayMode.REPEAT_ALL, _PlayMode.NORMAL, ], - (False, _mp_const.REPEAT_MODE_ALL): [ + (False, RepeatMode.ALL): [ _PlayMode.REPEAT_ALL, _PlayMode.REPEAT_ONE, _PlayMode.NORMAL, ], - (True, _mp_const.REPEAT_MODE_OFF): [ + (True, RepeatMode.OFF): [ _PlayMode.SHUFFLE, _PlayMode.RANDOM, _PlayMode.NORMAL, ], - (True, _mp_const.REPEAT_MODE_ONE): [ + (True, RepeatMode.ONE): [ _PlayMode.RANDOM, _PlayMode.SHUFFLE, _PlayMode.REPEAT_ONE, _PlayMode.NORMAL, ], - (True, _mp_const.REPEAT_MODE_ALL): [ + (True, RepeatMode.ALL): [ _PlayMode.RANDOM, _PlayMode.SHUFFLE, _PlayMode.REPEAT_ALL, diff --git a/homeassistant/components/dlna_dmr/media_player.py b/homeassistant/components/dlna_dmr/media_player.py index 156e8fdffef..ff09f018639 100644 --- a/homeassistant/components/dlna_dmr/media_player.py +++ b/homeassistant/components/dlna_dmr/media_player.py @@ -19,27 +19,16 @@ from typing_extensions import Concatenate, ParamSpec from homeassistant import config_entries from homeassistant.components import media_source, ssdp from homeassistant.components.media_player import ( + ATTR_MEDIA_EXTRA, BrowseMedia, MediaPlayerEntity, MediaPlayerEntityFeature, + MediaPlayerState, + MediaType, + RepeatMode, async_process_play_media_url, ) -from homeassistant.components.media_player.const import ( - ATTR_MEDIA_EXTRA, - REPEAT_MODE_ALL, - REPEAT_MODE_OFF, - REPEAT_MODE_ONE, -) -from homeassistant.const import ( - CONF_DEVICE_ID, - CONF_TYPE, - CONF_URL, - STATE_IDLE, - STATE_OFF, - STATE_ON, - STATE_PAUSED, - STATE_PLAYING, -) +from homeassistant.const import CONF_DEVICE_ID, CONF_TYPE, CONF_URL from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry, entity_registry from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -466,27 +455,27 @@ class DlnaDmrEntity(MediaPlayerEntity): return f"{self.udn}::{self.device_type}" @property - def state(self) -> str | None: + def state(self) -> MediaPlayerState | None: """State of the player.""" if not self._device or not self.available: - return STATE_OFF + return MediaPlayerState.OFF if self._device.transport_state is None: - return STATE_ON + return MediaPlayerState.ON if self._device.transport_state in ( TransportState.PLAYING, TransportState.TRANSITIONING, ): - return STATE_PLAYING + return MediaPlayerState.PLAYING if self._device.transport_state in ( TransportState.PAUSED_PLAYBACK, TransportState.PAUSED_RECORDING, ): - return STATE_PAUSED + return MediaPlayerState.PAUSED if self._device.transport_state == TransportState.VENDOR_DEFINED: # Unable to map this state to anything reasonable, so it's "Unknown" return None - return STATE_IDLE + return MediaPlayerState.IDLE @property def supported_features(self) -> int: @@ -586,7 +575,7 @@ class DlnaDmrEntity(MediaPlayerEntity): @catch_request_errors 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("Playing media: %s, %s, %s", media_type, media_id, kwargs) @@ -683,7 +672,7 @@ class DlnaDmrEntity(MediaPlayerEntity): """Enable/disable shuffle mode.""" assert self._device is not None - repeat = self.repeat or REPEAT_MODE_OFF + repeat = self.repeat or RepeatMode.OFF potential_play_modes = SHUFFLE_PLAY_MODES[(shuffle, repeat)] valid_play_modes = self._device.valid_play_modes @@ -698,7 +687,7 @@ class DlnaDmrEntity(MediaPlayerEntity): ) @property - def repeat(self) -> str | None: + def repeat(self) -> RepeatMode | None: """Return current repeat mode.""" if not self._device: return None @@ -710,15 +699,15 @@ class DlnaDmrEntity(MediaPlayerEntity): return None if play_mode == PlayMode.REPEAT_ONE: - return REPEAT_MODE_ONE + return RepeatMode.ONE if play_mode in (PlayMode.REPEAT_ALL, PlayMode.RANDOM): - return REPEAT_MODE_ALL + return RepeatMode.ALL - return REPEAT_MODE_OFF + return RepeatMode.OFF @catch_request_errors - async def async_set_repeat(self, repeat: str) -> None: + async def async_set_repeat(self, repeat: RepeatMode) -> None: """Set repeat mode.""" assert self._device is not None @@ -839,7 +828,7 @@ class DlnaDmrEntity(MediaPlayerEntity): return self._device.current_track_uri @property - def media_content_type(self) -> str | None: + def media_content_type(self) -> MediaType | None: """Content type of current playing media.""" if not self._device or not self._device.media_class: return None diff --git a/homeassistant/components/dlna_dmr/translations/cs.json b/homeassistant/components/dlna_dmr/translations/cs.json index c9087b82ab7..edbffb7c6ee 100644 --- a/homeassistant/components/dlna_dmr/translations/cs.json +++ b/homeassistant/components/dlna_dmr/translations/cs.json @@ -1,12 +1,21 @@ { "config": { "abort": { - "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" }, "flow_title": "{name}", "step": { "confirm": { "description": "Chcete za\u010d\u00edt nastavovat?" + }, + "user": { + "data": { + "host": "Hostitel" + } } } } diff --git a/homeassistant/components/dlna_dmr/translations/pt.json b/homeassistant/components/dlna_dmr/translations/pt.json index 49f47abb540..ea08ade3537 100644 --- a/homeassistant/components/dlna_dmr/translations/pt.json +++ b/homeassistant/components/dlna_dmr/translations/pt.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, "error": { "cannot_connect": "Falha na liga\u00e7\u00e3o" }, diff --git a/homeassistant/components/dlna_dmr/translations/zh-Hant.json b/homeassistant/components/dlna_dmr/translations/zh-Hant.json index 07293607685..571f6dcfd09 100644 --- a/homeassistant/components/dlna_dmr/translations/zh-Hant.json +++ b/homeassistant/components/dlna_dmr/translations/zh-Hant.json @@ -33,7 +33,7 @@ "host": "\u4e3b\u6a5f\u7aef" }, "description": "\u9078\u64c7\u88dd\u7f6e\u9032\u884c\u8a2d\u5b9a\u6216\u4fdd\u7559\u7a7a\u767d\u4ee5\u8f38\u5165 URL", - "title": "\u5df2\u767c\u73fe\u7684 DLNA DMR \u88dd\u7f6e" + "title": "\u81ea\u52d5\u641c\u7d22\u5230\u7684 DLNA DMR \u88dd\u7f6e" } } }, diff --git a/homeassistant/components/dlna_dms/const.py b/homeassistant/components/dlna_dms/const.py index 5d1c887fd49..8d4cb6352ee 100644 --- a/homeassistant/components/dlna_dms/const.py +++ b/homeassistant/components/dlna_dms/const.py @@ -5,7 +5,7 @@ from collections.abc import Mapping import logging from typing import Final -from homeassistant.components.media_player import const as _mp_const +from homeassistant.components.media_player import MediaClass LOGGER = logging.getLogger(__package__) @@ -41,41 +41,41 @@ PROTOCOL_ANY: Final = "*" STREAMABLE_PROTOCOLS: Final = [PROTOCOL_HTTP, PROTOCOL_RTSP, PROTOCOL_ANY] # Map UPnP object class to media_player media class -MEDIA_CLASS_MAP: Mapping[str, str] = { - "object": _mp_const.MEDIA_CLASS_URL, - "object.item": _mp_const.MEDIA_CLASS_URL, - "object.item.imageItem": _mp_const.MEDIA_CLASS_IMAGE, - "object.item.imageItem.photo": _mp_const.MEDIA_CLASS_IMAGE, - "object.item.audioItem": _mp_const.MEDIA_CLASS_MUSIC, - "object.item.audioItem.musicTrack": _mp_const.MEDIA_CLASS_MUSIC, - "object.item.audioItem.audioBroadcast": _mp_const.MEDIA_CLASS_MUSIC, - "object.item.audioItem.audioBook": _mp_const.MEDIA_CLASS_PODCAST, - "object.item.videoItem": _mp_const.MEDIA_CLASS_VIDEO, - "object.item.videoItem.movie": _mp_const.MEDIA_CLASS_MOVIE, - "object.item.videoItem.videoBroadcast": _mp_const.MEDIA_CLASS_TV_SHOW, - "object.item.videoItem.musicVideoClip": _mp_const.MEDIA_CLASS_VIDEO, - "object.item.playlistItem": _mp_const.MEDIA_CLASS_TRACK, - "object.item.textItem": _mp_const.MEDIA_CLASS_URL, - "object.item.bookmarkItem": _mp_const.MEDIA_CLASS_URL, - "object.item.epgItem": _mp_const.MEDIA_CLASS_EPISODE, - "object.item.epgItem.audioProgram": _mp_const.MEDIA_CLASS_MUSIC, - "object.item.epgItem.videoProgram": _mp_const.MEDIA_CLASS_VIDEO, - "object.container": _mp_const.MEDIA_CLASS_DIRECTORY, - "object.container.person": _mp_const.MEDIA_CLASS_ARTIST, - "object.container.person.musicArtist": _mp_const.MEDIA_CLASS_ARTIST, - "object.container.playlistContainer": _mp_const.MEDIA_CLASS_PLAYLIST, - "object.container.album": _mp_const.MEDIA_CLASS_ALBUM, - "object.container.album.musicAlbum": _mp_const.MEDIA_CLASS_ALBUM, - "object.container.album.photoAlbum": _mp_const.MEDIA_CLASS_ALBUM, - "object.container.genre": _mp_const.MEDIA_CLASS_GENRE, - "object.container.genre.musicGenre": _mp_const.MEDIA_CLASS_GENRE, - "object.container.genre.movieGenre": _mp_const.MEDIA_CLASS_GENRE, - "object.container.channelGroup": _mp_const.MEDIA_CLASS_CHANNEL, - "object.container.channelGroup.audioChannelGroup": _mp_const.MEDIA_TYPE_CHANNELS, - "object.container.channelGroup.videoChannelGroup": _mp_const.MEDIA_TYPE_CHANNELS, - "object.container.epgContainer": _mp_const.MEDIA_CLASS_DIRECTORY, - "object.container.storageSystem": _mp_const.MEDIA_CLASS_DIRECTORY, - "object.container.storageVolume": _mp_const.MEDIA_CLASS_DIRECTORY, - "object.container.storageFolder": _mp_const.MEDIA_CLASS_DIRECTORY, - "object.container.bookmarkFolder": _mp_const.MEDIA_CLASS_DIRECTORY, +MEDIA_CLASS_MAP: Mapping[str, MediaClass] = { + "object": MediaClass.URL, + "object.item": MediaClass.URL, + "object.item.imageItem": MediaClass.IMAGE, + "object.item.imageItem.photo": MediaClass.IMAGE, + "object.item.audioItem": MediaClass.MUSIC, + "object.item.audioItem.musicTrack": MediaClass.MUSIC, + "object.item.audioItem.audioBroadcast": MediaClass.MUSIC, + "object.item.audioItem.audioBook": MediaClass.PODCAST, + "object.item.videoItem": MediaClass.VIDEO, + "object.item.videoItem.movie": MediaClass.MOVIE, + "object.item.videoItem.videoBroadcast": MediaClass.TV_SHOW, + "object.item.videoItem.musicVideoClip": MediaClass.VIDEO, + "object.item.playlistItem": MediaClass.TRACK, + "object.item.textItem": MediaClass.URL, + "object.item.bookmarkItem": MediaClass.URL, + "object.item.epgItem": MediaClass.EPISODE, + "object.item.epgItem.audioProgram": MediaClass.MUSIC, + "object.item.epgItem.videoProgram": MediaClass.VIDEO, + "object.container": MediaClass.DIRECTORY, + "object.container.person": MediaClass.ARTIST, + "object.container.person.musicArtist": MediaClass.ARTIST, + "object.container.playlistContainer": MediaClass.PLAYLIST, + "object.container.album": MediaClass.ALBUM, + "object.container.album.musicAlbum": MediaClass.ALBUM, + "object.container.album.photoAlbum": MediaClass.ALBUM, + "object.container.genre": MediaClass.GENRE, + "object.container.genre.musicGenre": MediaClass.GENRE, + "object.container.genre.movieGenre": MediaClass.GENRE, + "object.container.channelGroup": MediaClass.CHANNEL, + "object.container.channelGroup.audioChannelGroup": MediaClass.CHANNEL, + "object.container.channelGroup.videoChannelGroup": MediaClass.CHANNEL, + "object.container.epgContainer": MediaClass.DIRECTORY, + "object.container.storageSystem": MediaClass.DIRECTORY, + "object.container.storageVolume": MediaClass.DIRECTORY, + "object.container.storageFolder": MediaClass.DIRECTORY, + "object.container.bookmarkFolder": MediaClass.DIRECTORY, } diff --git a/homeassistant/components/dlna_dms/dms.py b/homeassistant/components/dlna_dms/dms.py index d47f480132b..2fd1a85ebae 100644 --- a/homeassistant/components/dlna_dms/dms.py +++ b/homeassistant/components/dlna_dms/dms.py @@ -17,8 +17,7 @@ from didl_lite import didl_lite from homeassistant.backports.enum import StrEnum from homeassistant.components import ssdp -from homeassistant.components.media_player.const import MEDIA_CLASS_DIRECTORY -from homeassistant.components.media_player.errors import BrowseError +from homeassistant.components.media_player import BrowseError, MediaClass from homeassistant.components.media_source.error import Unresolvable from homeassistant.components.media_source.models import BrowseMediaSource, PlayMedia from homeassistant.config_entries import ConfigEntry @@ -518,7 +517,7 @@ class DmsDeviceSource: media_source = BrowseMediaSource( domain=DOMAIN, identifier=self._make_identifier(Action.SEARCH, query), - media_class=MEDIA_CLASS_DIRECTORY, + media_class=MediaClass.DIRECTORY, media_content_type="", title="Search results", can_play=False, diff --git a/homeassistant/components/dlna_dms/media_source.py b/homeassistant/components/dlna_dms/media_source.py index 84910b7ff67..c1245997c7a 100644 --- a/homeassistant/components/dlna_dms/media_source.py +++ b/homeassistant/components/dlna_dms/media_source.py @@ -12,13 +12,7 @@ Media identifiers can look like: from __future__ import annotations -from homeassistant.components.media_player.const import ( - MEDIA_CLASS_CHANNEL, - MEDIA_CLASS_DIRECTORY, - MEDIA_TYPE_CHANNEL, - MEDIA_TYPE_CHANNELS, -) -from homeassistant.components.media_player.errors import BrowseError +from homeassistant.components.media_player import BrowseError, MediaClass, MediaType from homeassistant.components.media_source.error import Unresolvable from homeassistant.components.media_source.models import ( BrowseMediaSource, @@ -82,20 +76,20 @@ class DmsMediaSource(MediaSource): base = BrowseMediaSource( domain=DOMAIN, identifier="", - media_class=MEDIA_CLASS_DIRECTORY, - media_content_type=MEDIA_TYPE_CHANNELS, + media_class=MediaClass.DIRECTORY, + media_content_type=MediaType.CHANNELS, title=self.name, can_play=False, can_expand=True, - children_media_class=MEDIA_CLASS_CHANNEL, + children_media_class=MediaClass.CHANNEL, ) base.children = [ BrowseMediaSource( domain=DOMAIN, identifier=f"{source_id}/{PATH_OBJECT_ID_FLAG}{ROOT_OBJECT_ID}", - media_class=MEDIA_CLASS_CHANNEL, - media_content_type=MEDIA_TYPE_CHANNEL, + media_class=MediaClass.CHANNEL, + media_content_type=MediaType.CHANNEL, title=source.name, can_play=False, can_expand=True, diff --git a/homeassistant/components/dlna_dms/translations/bg.json b/homeassistant/components/dlna_dms/translations/bg.json index 4356e0973c1..da5fbf4c01c 100644 --- a/homeassistant/components/dlna_dms/translations/bg.json +++ b/homeassistant/components/dlna_dms/translations/bg.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", + "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" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/dlna_dms/translations/zh-Hant.json b/homeassistant/components/dlna_dms/translations/zh-Hant.json index 404b9b29b9a..b2f5f3e147b 100644 --- a/homeassistant/components/dlna_dms/translations/zh-Hant.json +++ b/homeassistant/components/dlna_dms/translations/zh-Hant.json @@ -17,7 +17,7 @@ "host": "\u4e3b\u6a5f\u7aef" }, "description": "\u9078\u64c7\u6240\u8981\u8a2d\u5b9a\u7684\u88dd\u7f6e", - "title": "\u5df2\u767c\u73fe\u7684 DLNA DMA \u88dd\u7f6e" + "title": "\u81ea\u52d5\u641c\u7d22\u5230\u7684 DLNA DMA \u88dd\u7f6e" } } } diff --git a/homeassistant/components/dominos/__init__.py b/homeassistant/components/dominos/__init__.py index 31feaa7687e..ecfe7b65a7d 100644 --- a/homeassistant/components/dominos/__init__.py +++ b/homeassistant/components/dominos/__init__.py @@ -67,9 +67,9 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up is called when Home Assistant is loading our component.""" dominos = Dominos(hass, config) - component = EntityComponent(_LOGGER, DOMAIN, hass) + component = EntityComponent[DominosOrder](_LOGGER, DOMAIN, hass) hass.data[DOMAIN] = {} - entities = [] + entities: list[DominosOrder] = [] conf = config[DOMAIN] hass.services.register( diff --git a/homeassistant/components/doorbird/logbook.py b/homeassistant/components/doorbird/logbook.py index 3b1563c2880..f3beebe6971 100644 --- a/homeassistant/components/doorbird/logbook.py +++ b/homeassistant/components/doorbird/logbook.py @@ -3,7 +3,7 @@ from __future__ import annotations from typing import Any -from homeassistant.components.logbook.const import ( +from homeassistant.components.logbook import ( LOGBOOK_ENTRY_ENTITY_ID, LOGBOOK_ENTRY_MESSAGE, LOGBOOK_ENTRY_NAME, diff --git a/homeassistant/components/dsmr/translations/cs.json b/homeassistant/components/dsmr/translations/cs.json index 9ab3eefa6a6..d51fa24c335 100644 --- a/homeassistant/components/dsmr/translations/cs.json +++ b/homeassistant/components/dsmr/translations/cs.json @@ -1,11 +1,17 @@ { "config": { "abort": { - "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" + }, + "error": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" }, "step": { "setup_network": { "data": { + "host": "Hostitel", "port": "Port" } }, diff --git a/homeassistant/components/dsmr/translations/el.json b/homeassistant/components/dsmr/translations/el.json index 77f2ad8910d..44d598fe811 100644 --- a/homeassistant/components/dsmr/translations/el.json +++ b/homeassistant/components/dsmr/translations/el.json @@ -11,6 +11,8 @@ "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" }, "step": { + "one": "\u03ba\u03b5\u03bd\u03cc", + "other": "\u03ba\u03b5\u03bd\u03cc", "setup_network": { "data": { "dsmr_version": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03ad\u03ba\u03b4\u03bf\u03c3\u03b7\u03c2 DSMR", diff --git a/homeassistant/components/dsmr_reader/__init__.py b/homeassistant/components/dsmr_reader/__init__.py index 946be91d1a5..89b0da699e5 100644 --- a/homeassistant/components/dsmr_reader/__init__.py +++ b/homeassistant/components/dsmr_reader/__init__.py @@ -1 +1,19 @@ """The DSMR Reader component.""" + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant + +PLATFORMS = [Platform.SENSOR] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up the DSMR Reader integration.""" + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload the DSMR Reader integration.""" + # no data stored in hass.data + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/dsmr_reader/config_flow.py b/homeassistant/components/dsmr_reader/config_flow.py new file mode 100644 index 00000000000..2f08894d125 --- /dev/null +++ b/homeassistant/components/dsmr_reader/config_flow.py @@ -0,0 +1,40 @@ +"""Config flow to configure DSMR Reader.""" +from __future__ import annotations + +from collections.abc import Awaitable +import logging +from typing import Any + +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.config_entry_flow import DiscoveryFlowHandler + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +async def _async_has_devices(_: HomeAssistant) -> bool: + """MQTT is set as dependency, so that should be sufficient.""" + return True + + +class DsmrReaderFlowHandler(DiscoveryFlowHandler[Awaitable[bool]], domain=DOMAIN): + """Handle DSMR Reader config flow. The MQTT step is inherited from the parent class.""" + + VERSION = 1 + + def __init__(self) -> None: + """Set up the config flow.""" + super().__init__(DOMAIN, "DSMR Reader", _async_has_devices) + + async def async_step_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Confirm setup.""" + if user_input is None: + return self.async_show_form( + step_id="confirm", + ) + + return await super().async_step_confirm(user_input) diff --git a/homeassistant/components/dsmr_reader/const.py b/homeassistant/components/dsmr_reader/const.py new file mode 100644 index 00000000000..1f1679028d5 --- /dev/null +++ b/homeassistant/components/dsmr_reader/const.py @@ -0,0 +1,3 @@ +"""Constant values for DSMR Reader.""" + +DOMAIN = "dsmr_reader" diff --git a/homeassistant/components/dsmr_reader/manifest.json b/homeassistant/components/dsmr_reader/manifest.json index 0b631e790ed..df68e183fdf 100644 --- a/homeassistant/components/dsmr_reader/manifest.json +++ b/homeassistant/components/dsmr_reader/manifest.json @@ -1,8 +1,10 @@ { "domain": "dsmr_reader", "name": "DSMR Reader", + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/dsmr_reader", "dependencies": ["mqtt"], - "codeowners": ["@depl0y"], + "mqtt": ["dsmr/#"], + "codeowners": ["@depl0y", "@glodenox"], "iot_class": "local_push" } diff --git a/homeassistant/components/dsmr_reader/sensor.py b/homeassistant/components/dsmr_reader/sensor.py index 603b5682f42..7130380cbf5 100644 --- a/homeassistant/components/dsmr_reader/sensor.py +++ b/homeassistant/components/dsmr_reader/sensor.py @@ -3,15 +3,16 @@ from __future__ import annotations from homeassistant.components import mqtt from homeassistant.components.sensor import SensorEntity +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.core import HomeAssistant, callback 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 slugify +from .const import DOMAIN from .definitions import SENSORS, DSMRReaderSensorEntityDescription -DOMAIN = "dsmr_reader" - async def async_setup_platform( hass: HomeAssistant, @@ -19,8 +20,32 @@ async def async_setup_platform( async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: - """Set up DSMR Reader sensors.""" - async_add_entities(DSMRSensor(description) for description in SENSORS) + """Set up DSMR Reader sensors via configuration.yaml and show deprecation warning.""" + async_create_issue( + hass, + DOMAIN, + "deprecated_yaml", + breaks_in_ha_version="2022.12.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, + ) + ) + + +async def async_setup_entry( + _: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up DSMR Reader sensors from config entry.""" + async_add_entities(DSMRSensor(description, config_entry) for description in SENSORS) class DSMRSensor(SensorEntity): @@ -28,12 +53,15 @@ class DSMRSensor(SensorEntity): entity_description: DSMRReaderSensorEntityDescription - def __init__(self, description: DSMRReaderSensorEntityDescription) -> None: + def __init__( + self, description: DSMRReaderSensorEntityDescription, config_entry: ConfigEntry + ) -> None: """Initialize the sensor.""" self.entity_description = description slug = slugify(description.key.replace("/", "_")) self.entity_id = f"sensor.{slug}" + self._attr_unique_id = f"{config_entry.entry_id}-{slug}" async def async_added_to_hass(self) -> None: """Subscribe to MQTT events.""" diff --git a/homeassistant/components/dsmr_reader/strings.json b/homeassistant/components/dsmr_reader/strings.json new file mode 100644 index 00000000000..17e28cca884 --- /dev/null +++ b/homeassistant/components/dsmr_reader/strings.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" + }, + "step": { + "confirm": { + "description": "Make sure to configure the 'split topic' data sources in DSMR Reader." + } + } + }, + "issues": { + "deprecated_yaml": { + "title": "The DSMR Reader configuration is being removed", + "description": "Configuring DSMR Reader using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the DSMR Reader YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } + } +} diff --git a/homeassistant/components/dsmr_reader/translations/bg.json b/homeassistant/components/dsmr_reader/translations/bg.json new file mode 100644 index 00000000000..1c6120581b0 --- /dev/null +++ b/homeassistant/components/dsmr_reader/translations/bg.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "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." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dsmr_reader/translations/ca.json b/homeassistant/components/dsmr_reader/translations/ca.json new file mode 100644 index 00000000000..cc92e3ec9f1 --- /dev/null +++ b/homeassistant/components/dsmr_reader/translations/ca.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dsmr_reader/translations/de.json b/homeassistant/components/dsmr_reader/translations/de.json new file mode 100644 index 00000000000..963a3a74e2e --- /dev/null +++ b/homeassistant/components/dsmr_reader/translations/de.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." + }, + "step": { + "confirm": { + "description": "Stelle sicher, dass du die Datenquellen \u201eSplit Topic\u201c in DSMR Reader konfigurierst." + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Die Konfiguration von DSMR Reader mit YAML wird entfernt. \n\nDeine vorhandene YAML-Konfiguration wurde automatisch in die Benutzeroberfl\u00e4che importiert. \n\nEntferne die DSMR Reader YAML-Konfiguration aus deiner configuration.yaml-Datei und starte Home Assistant neu, um dieses Problem zu beheben.", + "title": "Die DSMR-Reader-Konfiguration wird entfernt" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dsmr_reader/translations/en.json b/homeassistant/components/dsmr_reader/translations/en.json new file mode 100644 index 00000000000..a2acb20b9b0 --- /dev/null +++ b/homeassistant/components/dsmr_reader/translations/en.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Already configured. Only a single configuration possible." + }, + "step": { + "confirm": { + "description": "Make sure to configure the 'split topic' data sources in DSMR Reader." + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Configuring DSMR Reader using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the DSMR Reader YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "title": "The DSMR Reader configuration is being removed" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dsmr_reader/translations/es.json b/homeassistant/components/dsmr_reader/translations/es.json new file mode 100644 index 00000000000..006c14b3522 --- /dev/null +++ b/homeassistant/components/dsmr_reader/translations/es.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." + }, + "step": { + "confirm": { + "description": "Aseg\u00farate de configurar las fuentes de datos de 'split topic' en DSMR Reader." + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Se va a eliminar la configuraci\u00f3n de DSMR Reader mediante YAML. \n\nTu configuraci\u00f3n YAML existente se ha importado a la IU autom\u00e1ticamente. \n\nElimina la configuraci\u00f3n YAML de DSMR Reader de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "title": "Se va a eliminar la configuraci\u00f3n de DSMR Reader" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dsmr_reader/translations/fr.json b/homeassistant/components/dsmr_reader/translations/fr.json new file mode 100644 index 00000000000..9761a37f4b0 --- /dev/null +++ b/homeassistant/components/dsmr_reader/translations/fr.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." + } + }, + "issues": { + "deprecated_yaml": { + "title": "La configuration de DSMR Reader sera bient\u00f4t supprim\u00e9e" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dsmr_reader/translations/hu.json b/homeassistant/components/dsmr_reader/translations/hu.json new file mode 100644 index 00000000000..4936b2363c5 --- /dev/null +++ b/homeassistant/components/dsmr_reader/translations/hu.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." + }, + "step": { + "confirm": { + "description": "Gy\u0151z\u0151dj\u00f6n meg r\u00f3la, hogy a DSMR Readerben be\u00e1ll\u00edtotta az 'split topic' adatforr\u00e1sokat." + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "A DSMR Reader YAML haszn\u00e1lat\u00e1val t\u00f6rt\u00e9n\u0151 konfigur\u00e1l\u00e1sa elt\u00e1vol\u00edt\u00e1sra ker\u00fcl.\n\nA megl\u00e9v\u0151 YAML konfigur\u00e1ci\u00f3 automatikusan import\u00e1l\u00e1sra ker\u00fclt a felhaszn\u00e1l\u00f3i fel\u00fcletre.\n\nA probl\u00e9ma megold\u00e1s\u00e1hoz t\u00e1vol\u00edtsa el a DSMR Reader YAML konfigur\u00e1ci\u00f3t a configuration.yaml f\u00e1jlb\u00f3l, \u00e9s ind\u00edtsa \u00fajra a Home Assistantot.", + "title": "A DSMR Reader konfigur\u00e1ci\u00f3ja elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dsmr_reader/translations/nl.json b/homeassistant/components/dsmr_reader/translations/nl.json new file mode 100644 index 00000000000..703ac8614c4 --- /dev/null +++ b/homeassistant/components/dsmr_reader/translations/nl.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dsmr_reader/translations/no.json b/homeassistant/components/dsmr_reader/translations/no.json new file mode 100644 index 00000000000..93a942bb163 --- /dev/null +++ b/homeassistant/components/dsmr_reader/translations/no.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig." + }, + "step": { + "confirm": { + "description": "S\u00f8rg for \u00e5 konfigurere \"delt emne\"-datakildene i DSMR Reader." + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Konfigurering av DSMR Reader med YAML blir fjernet. \n\n Din eksisterende YAML-konfigurasjon har blitt importert til brukergrensesnittet automatisk. \n\n Fjern DSMR Reader YAML-konfigurasjonen fra filen configuration.yaml og start Home Assistant p\u00e5 nytt for \u00e5 fikse dette problemet.", + "title": "DSMR Reader-konfigurasjonen fjernes" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dsmr_reader/translations/ru.json b/homeassistant/components/dsmr_reader/translations/ru.json new file mode 100644 index 00000000000..d610f616319 --- /dev/null +++ b/homeassistant/components/dsmr_reader/translations/ru.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e." + }, + "step": { + "confirm": { + "description": "\u041e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u0438 \u0434\u0430\u043d\u043d\u044b\u0445 'split topic' \u0432 DSMR Reader." + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 DSMR Reader \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 DSMR Reader \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/dsmr_reader/translations/zh-Hant.json b/homeassistant/components/dsmr_reader/translations/zh-Hant.json new file mode 100644 index 00000000000..99ee4a41b13 --- /dev/null +++ b/homeassistant/components/dsmr_reader/translations/zh-Hant.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" + }, + "step": { + "confirm": { + "description": "\u78ba\u5b9a\u65bc DSMR Reader \u5167\u8a2d\u5b9a 'split topic' \u8cc7\u6599\u4f86\u6e90\u3002" + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a\u7684 DSMR Reader \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 DSMR Reader YAML \u8a2d\u5b9a\u4e26\u91cd\u65b0\u555f\u52d5 Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "DSMR Reader \u8a2d\u5b9a\u5373\u5c07\u79fb\u9664" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dunehd/config_flow.py b/homeassistant/components/dunehd/config_flow.py index b5a656716b0..fac2e245633 100644 --- a/homeassistant/components/dunehd/config_flow.py +++ b/homeassistant/components/dunehd/config_flow.py @@ -1,8 +1,6 @@ """Adds config flow for Dune HD integration.""" from __future__ import annotations -import ipaddress -import re from typing import Any from pdunehd import DuneHDPlayer @@ -11,23 +9,11 @@ import voluptuous as vol from homeassistant import config_entries, exceptions from homeassistant.const import CONF_HOST from homeassistant.data_entry_flow import FlowResult +from homeassistant.util.network import is_host_valid from .const import DOMAIN -def host_valid(host: str) -> bool: - """Return True if hostname or IP address is valid.""" - try: - if ipaddress.ip_address(host).version in (4, 6): - return True - except ValueError: - pass - if len(host) > 253: - return False - allowed = re.compile(r"(?!-)[A-Z\d\-\_]{1,63}(? str | None: + def state(self) -> MediaPlayerState: """Return player state.""" - state = STATE_OFF + state = MediaPlayerState.OFF if "playback_position" in self._state: - state = STATE_PLAYING + state = MediaPlayerState.PLAYING if self._state.get("player_state") in ("playing", "buffering", "photo_viewer"): - state = STATE_PLAYING + state = MediaPlayerState.PLAYING if int(self._state.get("playback_speed", 1234)) == 0: - state = STATE_PAUSED + state = MediaPlayerState.PAUSED if self._state.get("player_state") == "navigator": - state = STATE_ON + state = MediaPlayerState.ON return state @property diff --git a/homeassistant/components/ecobee/climate.py b/homeassistant/components/ecobee/climate.py index 6a96b5418b6..16870d76902 100644 --- a/homeassistant/components/ecobee/climate.py +++ b/homeassistant/components/ecobee/climate.py @@ -6,14 +6,14 @@ from typing import Any import voluptuous as vol -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, FAN_AUTO, FAN_ON, PRESET_AWAY, PRESET_NONE, + ClimateEntity, ClimateEntityFeature, HVACAction, HVACMode, @@ -33,7 +33,7 @@ from homeassistant.helpers import entity_platform import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.util.temperature import convert +from homeassistant.util.unit_conversion import TemperatureConverter from .const import _LOGGER, DOMAIN, ECOBEE_MODEL_TO_NAME, MANUFACTURER from .util import ecobee_date, ecobee_time @@ -763,12 +763,12 @@ class Thermostat(ClimateEntity): def create_vacation(self, service_data): """Create a vacation with user-specified parameters.""" vacation_name = service_data[ATTR_VACATION_NAME] - cool_temp = convert( + cool_temp = TemperatureConverter.convert( service_data[ATTR_COOL_TEMP], self.hass.config.units.temperature_unit, TEMP_FAHRENHEIT, ) - heat_temp = convert( + heat_temp = TemperatureConverter.convert( service_data[ATTR_HEAT_TEMP], self.hass.config.units.temperature_unit, TEMP_FAHRENHEIT, diff --git a/homeassistant/components/ecobee/humidifier.py b/homeassistant/components/ecobee/humidifier.py index 230e456dc60..93d658a9ddc 100644 --- a/homeassistant/components/ecobee/humidifier.py +++ b/homeassistant/components/ecobee/humidifier.py @@ -4,14 +4,12 @@ from __future__ import annotations from datetime import timedelta from homeassistant.components.humidifier import ( - HumidifierDeviceClass, - HumidifierEntity, - HumidifierEntityFeature, -) -from homeassistant.components.humidifier.const import ( DEFAULT_MAX_HUMIDITY, DEFAULT_MIN_HUMIDITY, MODE_AUTO, + HumidifierDeviceClass, + HumidifierEntity, + HumidifierEntityFeature, ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant diff --git a/homeassistant/components/econet/climate.py b/homeassistant/components/econet/climate.py index 9fba4883644..bda462285fc 100644 --- a/homeassistant/components/econet/climate.py +++ b/homeassistant/components/econet/climate.py @@ -4,14 +4,14 @@ from typing import Any from pyeconet.equipment import EquipmentType from pyeconet.equipment.thermostat import ThermostatFanMode, ThermostatOperationMode -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, FAN_AUTO, FAN_HIGH, FAN_LOW, FAN_MEDIUM, + ClimateEntity, ClimateEntityFeature, HVACMode, ) diff --git a/homeassistant/components/ecowitt/manifest.json b/homeassistant/components/ecowitt/manifest.json index 224b4440e36..9ba16231867 100644 --- a/homeassistant/components/ecowitt/manifest.json +++ b/homeassistant/components/ecowitt/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ecowitt", "dependencies": ["webhook"], - "requirements": ["aioecowitt==2022.09.1"], + "requirements": ["aioecowitt==2022.09.3"], "codeowners": ["@pvizeli"], "iot_class": "local_push" } diff --git a/homeassistant/components/ecowitt/sensor.py b/homeassistant/components/ecowitt/sensor.py index bb580b6d4b7..a644cd3ca7a 100644 --- a/homeassistant/components/ecowitt/sensor.py +++ b/homeassistant/components/ecowitt/sensor.py @@ -196,6 +196,11 @@ ECOWITT_SENSORS_MAPPING: Final = { native_unit_of_measurement=PRESSURE_INHG, state_class=SensorStateClass.MEASUREMENT, ), + EcoWittSensorTypes.PERCENTAGE: SensorEntityDescription( + key="PERCENTAGE", + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), } diff --git a/homeassistant/components/ecowitt/translations/bg.json b/homeassistant/components/ecowitt/translations/bg.json new file mode 100644 index 00000000000..92e4c1888a4 --- /dev/null +++ b/homeassistant/components/ecowitt/translations/bg.json @@ -0,0 +1,13 @@ +{ + "config": { + "error": { + "invalid_port": "\u041f\u043e\u0440\u0442\u044a\u0442 \u0432\u0435\u0447\u0435 \u0441\u0435 \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430.", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "user": { + "description": "\u0421\u0438\u0433\u0443\u0440\u043d\u0438 \u043b\u0438 \u0441\u0442\u0435, \u0447\u0435 \u0438\u0441\u043a\u0430\u0442\u0435 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 Ecowitt?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ecowitt/translations/ca.json b/homeassistant/components/ecowitt/translations/ca.json new file mode 100644 index 00000000000..5dd00992145 --- /dev/null +++ b/homeassistant/components/ecowitt/translations/ca.json @@ -0,0 +1,20 @@ +{ + "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')." + }, + "error": { + "invalid_port": "Aquest port ja est\u00e0 en \u00fas.", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "path": "Cam\u00ed amb testimoni de seguretat", + "port": "Port d'escolta" + }, + "description": "Est\u00e0s segur que vols configurar Ecowitt?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ecowitt/translations/cs.json b/homeassistant/components/ecowitt/translations/cs.json new file mode 100644 index 00000000000..b9301d0099d --- /dev/null +++ b/homeassistant/components/ecowitt/translations/cs.json @@ -0,0 +1,8 @@ +{ + "config": { + "error": { + "invalid_port": "Port je ji\u017e pou\u017e\u00edv\u00e1n", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ecowitt/translations/de.json b/homeassistant/components/ecowitt/translations/de.json new file mode 100644 index 00000000000..752363c97e7 --- /dev/null +++ b/homeassistant/components/ecowitt/translations/de.json @@ -0,0 +1,20 @@ +{ + "config": { + "create_entry": { + "default": "Um die Integration abzuschlie\u00dfen, verwende die Ecowitt App (auf deinem Telefon) oder rufe die Ecowitt WebUI in einem Browser unter der IP-Adresse der Station auf.\n\nW\u00e4hle deine Station -> Men\u00fc Andere -> DIY Upload Servers. Klicke auf \"Weiter\" und w\u00e4hle \"Angepasst\".\n\n- Server IP: `{server}`\n- Pfad: `{path}`\n- Anschluss: `{port}`\n\nKlicke auf \"Speichern\"." + }, + "error": { + "invalid_port": "Port wird bereits verwendet.", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "path": "Pfad mit Sicherheits-Token", + "port": "Listening-Port" + }, + "description": "M\u00f6chtest du Ecowitt wirklich einrichten?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ecowitt/translations/el.json b/homeassistant/components/ecowitt/translations/el.json new file mode 100644 index 00000000000..e8022f8808a --- /dev/null +++ b/homeassistant/components/ecowitt/translations/el.json @@ -0,0 +1,20 @@ +{ + "config": { + "create_entry": { + "default": "\u0393\u03b9\u03b1 \u03bd\u03b1 \u03bf\u03bb\u03bf\u03ba\u03bb\u03b7\u03c1\u03ce\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2, \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae Ecowitt App (\u03c3\u03c4\u03bf \u03c4\u03b7\u03bb\u03ad\u03c6\u03c9\u03bd\u03cc \u03c3\u03b1\u03c2) \u03ae \u03b1\u03c0\u03bf\u03ba\u03c4\u03ae\u03c3\u03c4\u03b5 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7 \u03c3\u03c4\u03bf Ecowitt WebUI \u03c3\u03b5 \u03ad\u03bd\u03b1 \u03c0\u03c1\u03cc\u03b3\u03c1\u03b1\u03bc\u03bc\u03b1 \u03c0\u03b5\u03c1\u03b9\u03ae\u03b3\u03b7\u03c3\u03b7\u03c2 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP \u03c4\u03bf\u03c5 \u03c3\u03c4\u03b1\u03b8\u03bc\u03bf\u03cd.\n\n\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03bf \u03c3\u03c4\u03b1\u03b8\u03bc\u03cc \u03c3\u03b1\u03c2 -> \u039c\u03b5\u03bd\u03bf\u03cd \u0386\u03bb\u03bb\u03bf\u03b9 -> \u0395\u03be\u03c5\u03c0\u03b7\u03c1\u03b5\u03c4\u03b7\u03c4\u03ad\u03c2 \u03c6\u03cc\u03c1\u03c4\u03c9\u03c3\u03b7\u03c2 DIY. \u03a0\u03b1\u03c4\u03ae\u03c3\u03c4\u03b5 next \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 'Customized' (\u03a0\u03c1\u03bf\u03c3\u03b1\u03c1\u03bc\u03bf\u03c3\u03bc\u03ad\u03bd\u03bf)\n\n- IP \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae: `{server}`\n- \u0394\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae: `{path}`\n- \u0398\u03cd\u03c1\u03b1: `{port}`\n\n\u039a\u03ac\u03bd\u03c4\u03b5 \u03ba\u03bb\u03b9\u03ba \u03c3\u03c4\u03b7\u03bd '\u0391\u03c0\u03bf\u03b8\u03ae\u03ba\u03b5\u03c5\u03c3\u03b7'." + }, + "error": { + "invalid_port": "\u0397 \u03b8\u03cd\u03c1\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03ae\u03b4\u03b7.", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, + "step": { + "user": { + "data": { + "path": "\u0394\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae \u03bc\u03b5 \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc \u03b1\u03c3\u03c6\u03b1\u03bb\u03b5\u03af\u03b1\u03c2", + "port": "\u0398\u03cd\u03c1\u03b1 \u03b1\u03ba\u03c1\u03cc\u03b1\u03c3\u03b7\u03c2" + }, + "description": "\u03a0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03ba\u03c4\u03b5\u03bb\u03b5\u03c3\u03c4\u03bf\u03cd\u03bd \u03c4\u03b1 \u03b1\u03ba\u03cc\u03bb\u03bf\u03c5\u03b8\u03b1 \u03b2\u03ae\u03bc\u03b1\u03c4\u03b1 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03ae\u03bd \u03c4\u03b7\u03bd \u03b5\u03bd\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7. \n\n \u03a7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae Ecowitt (\u03c3\u03c4\u03bf \u03c4\u03b7\u03bb\u03ad\u03c6\u03c9\u03bd\u03cc \u03c3\u03b1\u03c2) \u03ae \u03b1\u03c0\u03bf\u03ba\u03c4\u03ae\u03c3\u03c4\u03b5 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7 \u03c3\u03c4\u03bf Ecowitt WebUI \u03c3\u03b5 \u03ad\u03bd\u03b1 \u03c0\u03c1\u03cc\u03b3\u03c1\u03b1\u03bc\u03bc\u03b1 \u03c0\u03b5\u03c1\u03b9\u03ae\u03b3\u03b7\u03c3\u03b7\u03c2 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP \u03c4\u03bf\u03c5 \u03c3\u03c4\u03b1\u03b8\u03bc\u03bf\u03cd.\n \u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03bf \u03c3\u03c4\u03b1\u03b8\u03bc\u03cc \u03c3\u03b1\u03c2 - > \u039c\u03b5\u03bd\u03bf\u03cd \u0386\u03bb\u03bb\u03b1 - > \u0394\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ad\u03c2 \u03bc\u03b5\u03c4\u03b1\u03c6\u03cc\u03c1\u03c4\u03c9\u03c3\u03b7\u03c2 DIY.\n \u03a0\u03b1\u03c4\u03ae\u03c3\u03c4\u03b5 \u03b5\u03c0\u03cc\u03bc\u03b5\u03bd\u03bf \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \"\u03a0\u03c1\u03bf\u03c3\u03b1\u03c1\u03bc\u03bf\u03c3\u03bc\u03ad\u03bd\u03bf\" \n\n \u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03bf \u03c0\u03c1\u03c9\u03c4\u03cc\u03ba\u03bf\u03bb\u03bb\u03bf Ecowitt \u03ba\u03b1\u03b9 \u03b2\u03ac\u03bb\u03c4\u03b5 \u03c4\u03bf ip/hostname \u03c4\u03bf\u03c5 hass server \u03c3\u03b1\u03c2.\n \u0397 \u03b4\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c4\u03b1\u03b9\u03c1\u03b9\u03ac\u03b6\u03b5\u03b9, \u03bc\u03c0\u03bf\u03c1\u03b5\u03af\u03c4\u03b5 \u03bd\u03b1 \u03b1\u03bd\u03c4\u03b9\u03b3\u03c1\u03ac\u03c8\u03b5\u03c4\u03b5 \u03bc\u03b5 \u03b1\u03c3\u03c6\u03b1\u03bb\u03ad\u03c2 \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc /.\n \u0391\u03c0\u03bf\u03b8\u03ae\u03ba\u03b5\u03c5\u03c3\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2. \u03a4\u03bf Ecowitt \u03b8\u03b1 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03c3\u03c4\u03b7 \u03c3\u03c5\u03bd\u03ad\u03c7\u03b5\u03b9\u03b1 \u03bd\u03b1 \u03b1\u03c1\u03c7\u03af\u03c3\u03b5\u03b9 \u03bd\u03b1 \u03c0\u03c1\u03bf\u03c3\u03c0\u03b1\u03b8\u03b5\u03af \u03bd\u03b1 \u03c3\u03c4\u03b5\u03af\u03bb\u03b5\u03b9 \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03b1 \u03c3\u03c4\u03bf\u03bd \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae \u03c3\u03b1\u03c2." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ecowitt/translations/en.json b/homeassistant/components/ecowitt/translations/en.json index b8ce69c10b8..77b50cd6462 100644 --- a/homeassistant/components/ecowitt/translations/en.json +++ b/homeassistant/components/ecowitt/translations/en.json @@ -3,8 +3,16 @@ "create_entry": { "default": "To finish setting up the integration, use the Ecowitt App (on your phone) or access the Ecowitt WebUI in a browser at the station IP address.\n\nPick your station -> Menu Others -> DIY Upload Servers. Hit next and select 'Customized'\n\n- Server IP: `{server}`\n- Path: `{path}`\n- Port: `{port}`\n\nClick on 'Save'." }, + "error": { + "invalid_port": "Port is already used.", + "unknown": "Unexpected error" + }, "step": { "user": { + "data": { + "path": "Path with Security token", + "port": "Listening port" + }, "description": "Are you sure you want to set up Ecowitt?" } } diff --git a/homeassistant/components/ecowitt/translations/es.json b/homeassistant/components/ecowitt/translations/es.json new file mode 100644 index 00000000000..94e21c4782c --- /dev/null +++ b/homeassistant/components/ecowitt/translations/es.json @@ -0,0 +1,20 @@ +{ + "config": { + "create_entry": { + "default": "Para terminar de configurar la integraci\u00f3n, usa la aplicaci\u00f3n Ecowitt (en tu tel\u00e9fono) o accede a Ecowitt WebUI en un navegador en la direcci\u00f3n IP de la estaci\u00f3n. \n\nElige tu estaci\u00f3n - > Men\u00fa Otros - > Servidores de carga de bricolaje. Presiona siguiente y selecciona 'Personalizado' \n\n- IP del servidor: `{server}`\n- Ruta: `{path}`\n- Puerto: `{port}` \n\nHaz clic en 'Guardar'." + }, + "error": { + "invalid_port": "El puerto ya est\u00e1 en uso.", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "path": "Ruta con token de seguridad", + "port": "Puerto de escucha" + }, + "description": "\u00bfEst\u00e1s seguro de que quieres configurar Ecowitt?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ecowitt/translations/et.json b/homeassistant/components/ecowitt/translations/et.json new file mode 100644 index 00000000000..e132191ca37 --- /dev/null +++ b/homeassistant/components/ecowitt/translations/et.json @@ -0,0 +1,20 @@ +{ + "config": { + "create_entry": { + "default": "Sidumise seadistamise l\u00f5petamiseks kasuta Ecowitti rakendust (telefonis) v\u00f5i sisene Ecowitt WebUI-sse brauseris jaama IP-aadressil.\n\nVali oma jaam -> men\u00fc\u00fc Muud -> DIY Upload Servers. Vajuta nuppu next ja vali 'Customized' (kohandatud)\n\n- Serveri IP: `{server}`\n- Path: `{path}`\n- Port: `{port}`\n\nVajuta nupule 'Save'." + }, + "error": { + "invalid_port": "Port on juba kasutusel.", + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "user": { + "data": { + "path": "Turvam\u00e4rgiga asukoht", + "port": "Kuulamisport" + }, + "description": "Kas oled kindel, et soovid Ecowitti seadistada?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ecowitt/translations/fr.json b/homeassistant/components/ecowitt/translations/fr.json new file mode 100644 index 00000000000..c4f3bfbb937 --- /dev/null +++ b/homeassistant/components/ecowitt/translations/fr.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "invalid_port": "Le port est d\u00e9j\u00e0 utilis\u00e9.", + "unknown": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "port": "Port d'\u00e9coute" + }, + "description": "Voulez-vous vraiment configurer Ecowitt\u00a0?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ecowitt/translations/hu.json b/homeassistant/components/ecowitt/translations/hu.json new file mode 100644 index 00000000000..920602311bf --- /dev/null +++ b/homeassistant/components/ecowitt/translations/hu.json @@ -0,0 +1,20 @@ +{ + "config": { + "create_entry": { + "default": "Az integr\u00e1ci\u00f3 be\u00e1ll\u00edt\u00e1s\u00e1nak befejez\u00e9s\u00e9hez haszn\u00e1lja az Ecowitt alkalmaz\u00e1st (a telefonj\u00e1n), vagy l\u00e9pjen be az Ecowitt WebUI-ba egy b\u00f6ng\u00e9sz\u0151ben az \u00e1llom\u00e1s IP-c\u00edm\u00e9n.\n\nV\u00e1lassza ki az \u00e1llom\u00e1s\u00e1t -> 'Others' men\u00fc -> 'DIY Upload Servers'. Nyomja meg a 'Next' gombot, \u00e9s v\u00e1lassza a 'Customized' lehet\u0151s\u00e9get.\n\n- Szerver IP: `{server}`\n- \u00datvonal: `{path}`\n- Port: `{port}`\n\nKattintson a 'Save' gombra." + }, + "error": { + "invalid_port": "A port m\u00e1r haszn\u00e1latban van.", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "step": { + "user": { + "data": { + "path": "Biztons\u00e1gi tokennel ell\u00e1tott el\u00e9r\u00e9si \u00fatvonal", + "port": "Figyel\u0151port" + }, + "description": "Biztos benne, hogy be szeretn\u00e9 \u00e1ll\u00edtani: Ecowitt?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ecowitt/translations/id.json b/homeassistant/components/ecowitt/translations/id.json new file mode 100644 index 00000000000..36479f19729 --- /dev/null +++ b/homeassistant/components/ecowitt/translations/id.json @@ -0,0 +1,20 @@ +{ + "config": { + "create_entry": { + "default": "Untuk menyelesaikan pengaturan integrasi, gunakan Ecowitt App (pada ponsel Anda) atau akses Ecowitt WebUI di browser pada alamat IP stasiun.\n\nPilih stasiun Anda -> Menu Others -> DIY Upload Servers. Tekan 'Next' dan pilih 'Customized'\n\n- Server IP: `{server}`\n- Path: `{path}`\n- Port: `{port}`\n\nKlik 'Simpan'." + }, + "error": { + "invalid_port": "Port sudah digunakan.", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "user": { + "data": { + "path": "Jalur dengan token Keamanan", + "port": "Port mendengarkan" + }, + "description": "Yakin ingin menyiapkan Ecowitt?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ecowitt/translations/it.json b/homeassistant/components/ecowitt/translations/it.json new file mode 100644 index 00000000000..91cfcd52fe2 --- /dev/null +++ b/homeassistant/components/ecowitt/translations/it.json @@ -0,0 +1,20 @@ +{ + "config": { + "create_entry": { + "default": "Per completare la configurazione dell'integrazione, utilizzare l'App Ecowitt (sul telefono) o accedere all'Ecowitt WebUI in un browser all'indirizzo IP della stazione. \n\nScegli la tua stazione - > Menu Altri - > Server di caricamento fai-da-te. Premi Avanti e seleziona \"Personalizzata\" \n\n - IP del server: `{server}`\n - Percorso: `{path}`\n - Porta: `{port}` \n\n Fai clic su \"Salva\"." + }, + "error": { + "invalid_port": "La porta \u00e8 gi\u00e0 utilizzata.", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "path": "Percorso con token di sicurezza", + "port": "Porta di ascolto" + }, + "description": "Sei sicuro di voler configurare Ecowitt?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ecowitt/translations/ja.json b/homeassistant/components/ecowitt/translations/ja.json new file mode 100644 index 00000000000..0853f5068b1 --- /dev/null +++ b/homeassistant/components/ecowitt/translations/ja.json @@ -0,0 +1,20 @@ +{ + "config": { + "create_entry": { + "default": "\u7d71\u5408\u306e\u8a2d\u5b9a\u3092\u5b8c\u4e86\u3059\u308b\u306b\u306f\u3001Ecowitt \u30a2\u30d7\u30ea (\u96fb\u8a71\u3067) \u3092\u4f7f\u7528\u3059\u308b\u304b\u3001\u30b9\u30c6\u30fc\u30b7\u30e7\u30f3\u306e IP \u30a2\u30c9\u30ec\u30b9\u3067\u30d6\u30e9\u30a6\u30b6\u30fc\u3067 Ecowitt WebUI \u306b\u30a2\u30af\u30bb\u30b9\u3057\u307e\u3059\u3002 \n\n\u30b9\u30c6\u30fc\u30b7\u30e7\u30f3\u3092\u9078\u629e - >\u30e1\u30cb\u30e5\u30fc\u306e [\u305d\u306e\u4ed6] - > DIY \u30a2\u30c3\u30d7\u30ed\u30fc\u30c9 \u30b5\u30fc\u30d0\u30fc] \u3092\u9078\u629e\u3057\u307e\u3059\u3002\u6b21\u306b\u30d2\u30c3\u30c8\u3057\u3001\u300c\u30ab\u30b9\u30bf\u30de\u30a4\u30ba\u300d\u3092\u9078\u629e\u3057\u307e\u3059\n\n - \u30b5\u30fc\u30d0\u30fc IP: ` {server} `\n - \u30d1\u30b9: ` {path} `\n - \u30dd\u30fc\u30c8: ` {port} ` \n\n \u300c\u4fdd\u5b58\u300d\u3092\u30af\u30ea\u30c3\u30af\u3057\u307e\u3059\u3002" + }, + "error": { + "invalid_port": "\u30dd\u30fc\u30c8\u306f\u3059\u3067\u306b\u4f7f\u7528\u3055\u308c\u3066\u3044\u307e\u3059\u3002", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, + "step": { + "user": { + "data": { + "path": "\u30bb\u30ad\u30e5\u30ea\u30c6\u30a3\u30c8\u30fc\u30af\u30f3\u3092\u542b\u3080\u30d1\u30b9", + "port": "\u30ea\u30b9\u30cb\u30f3\u30b0\u30dd\u30fc\u30c8" + }, + "description": "Ecowitt\u3092\u3001\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u3066\u3082\u3088\u308d\u3057\u3044\u3067\u3059\u304b\uff1f" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ecowitt/translations/nl.json b/homeassistant/components/ecowitt/translations/nl.json new file mode 100644 index 00000000000..1090e946378 --- /dev/null +++ b/homeassistant/components/ecowitt/translations/nl.json @@ -0,0 +1,8 @@ +{ + "config": { + "error": { + "invalid_port": "Poort wordt al gebruikt.", + "unknown": "Onverwachte fout" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ecowitt/translations/no.json b/homeassistant/components/ecowitt/translations/no.json new file mode 100644 index 00000000000..61372b6f49f --- /dev/null +++ b/homeassistant/components/ecowitt/translations/no.json @@ -0,0 +1,20 @@ +{ + "config": { + "create_entry": { + "default": "For \u00e5 fullf\u00f8re konfigureringen av integrasjonen, bruk Ecowitt-appen (p\u00e5 telefonen) eller g\u00e5 til Ecowitt WebUI i en nettleser p\u00e5 stasjonens IP-adresse. \n\n Velg stasjonen din - > Meny Andre - > DIY-opplastingsservere. Trykk neste og velg \"Tilpasset\" \n\n - Server IP: ` {server} `\n - Bane: ` {path} `\n - Port: ` {port} ` \n\n Klikk p\u00e5 'Lagre'." + }, + "error": { + "invalid_port": "Porten er allerede i bruk.", + "unknown": "Uventet feil" + }, + "step": { + "user": { + "data": { + "path": "Bane med sikkerhetstoken", + "port": "Lytteport" + }, + "description": "Er du sikker p\u00e5 at du vil sette opp Ecowitt?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ecowitt/translations/pl.json b/homeassistant/components/ecowitt/translations/pl.json new file mode 100644 index 00000000000..64fb3e6e4ef --- /dev/null +++ b/homeassistant/components/ecowitt/translations/pl.json @@ -0,0 +1,20 @@ +{ + "config": { + "create_entry": { + "default": "Aby zako\u0144czy\u0107 konfiguracj\u0119 integracji, u\u017cyj aplikacji Ecowitt (na telefonie) lub uzyskaj dost\u0119p do Ecowitt WebUI w przegl\u0105darce pod adresem IP stacji. \n\nWybierz swoj\u0105 stacj\u0119 - > Menu \"Others\" - > DIY Upload Servers. Kliknij dalej i wybierz \"Customized\" \n\n- IP serwera: `{server}`\n- \u015acie\u017cka: `{path}`\n- Port: `{port}` \n\nKliknij \u201eZapisz\u201d." + }, + "error": { + "invalid_port": "Port jest ju\u017c u\u017cywany.", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "user": { + "data": { + "path": "\u015acie\u017cka do tokena bezpiecze\u0144stwa", + "port": "Port nas\u0142uchiwania" + }, + "description": "Czy chcesz rozpocz\u0105\u0107 konfiguracj\u0119?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ecowitt/translations/pt-BR.json b/homeassistant/components/ecowitt/translations/pt-BR.json new file mode 100644 index 00000000000..b0c23d7a35d --- /dev/null +++ b/homeassistant/components/ecowitt/translations/pt-BR.json @@ -0,0 +1,20 @@ +{ + "config": { + "create_entry": { + "default": "Para finalizar a configura\u00e7\u00e3o da integra\u00e7\u00e3o, use o app Ecowitt (no seu smartphone) ou acesse o Ecowitt WebUI em um navegador no endere\u00e7o IP da esta\u00e7\u00e3o. \n\n Escolha sua esta\u00e7\u00e3o - > Menu Outros - > Servidores de Upload DIY. Clique em pr\u00f3ximo e selecione 'Personalizado' \n\n - IP do servidor: `{server}`\n - Caminho: `{path}`\n - Porta: `{port}` \n\n Clique em 'Salvar'." + }, + "error": { + "invalid_port": "A porta j\u00e1 \u00e9 usada.", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "path": "Caminho com token de seguran\u00e7a", + "port": "Porta de escuta" + }, + "description": "Tem certeza de que deseja configurar o Ecowitt?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ecowitt/translations/pt.json b/homeassistant/components/ecowitt/translations/pt.json new file mode 100644 index 00000000000..71a66816a83 --- /dev/null +++ b/homeassistant/components/ecowitt/translations/pt.json @@ -0,0 +1,19 @@ +{ + "config": { + "create_entry": { + "default": "Para finalizar a configura\u00e7\u00e3o da integra\u00e7\u00e3o, use o Ecowitt App (no seu telefone) ou acesse o Ecowitt WebUI em um navegador no endere\u00e7o IP da esta\u00e7\u00e3o. \n\n Escolha sua esta\u00e7\u00e3o - > Menu Outros - > Servidores de Upload DIY. Clique em pr\u00f3ximo e selecione 'Personalizado' \n\n - IP do servidor: ` {server} `\n - Caminho: ` {path} `\n - Porta: ` {port} ` \n\n Clique em 'Salvar'." + }, + "error": { + "invalid_port": "A porta j\u00e1 \u00e9 usada." + }, + "step": { + "user": { + "data": { + "path": "Caminho com token de seguran\u00e7a", + "port": "Porta de escuta" + }, + "description": "Tem certeza de que deseja configurar o Ecowitt?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ecowitt/translations/ru.json b/homeassistant/components/ecowitt/translations/ru.json new file mode 100644 index 00000000000..97532b0726b --- /dev/null +++ b/homeassistant/components/ecowitt/translations/ru.json @@ -0,0 +1,20 @@ +{ + "config": { + "create_entry": { + "default": "\u0427\u0442\u043e\u0431\u044b \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 Ecowitt (\u043d\u0430 \u0442\u0435\u043b\u0435\u0444\u043e\u043d\u0435) \u0438\u043b\u0438 \u0432\u043e\u0439\u0434\u0438\u0442\u0435 \u0432 \u0432\u0435\u0431-\u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441 Ecowitt \u0432 \u0431\u0440\u0430\u0443\u0437\u0435\u0440\u0435 \u043f\u043e IP-\u0430\u0434\u0440\u0435\u0441\u0443 \u0441\u0442\u0430\u043d\u0446\u0438\u0438. \n\n\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u0432\u043e\u044e \u0441\u0442\u0430\u043d\u0446\u0438\u044e - > \u041c\u0435\u043d\u044e 'Others' - > 'DIY Upload Servers'. \u041d\u0430\u0436\u043c\u0438\u0442\u0435 'Next' \u0438 \u0432\u044b\u0431\u0435\u0440\u0438\u0442\u0435 'Customized'. \n\n- IP-\u0430\u0434\u0440\u0435\u0441 \u0441\u0435\u0440\u0432\u0435\u0440\u0430: `{server}`\n- \u041f\u0443\u0442\u044c: `{path}`\n- \u041f\u043e\u0440\u0442: `{port}` \n\n\u041d\u0430\u0436\u043c\u0438\u0442\u0435 'Save'." + }, + "error": { + "invalid_port": "\u041f\u043e\u0440\u0442 \u0443\u0436\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "user": { + "data": { + "path": "\u041f\u0443\u0442\u044c \u0441 \u0442\u043e\u043a\u0435\u043d\u043e\u043c \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u0441\u0442\u0438", + "port": "\u041f\u043e\u0440\u0442 \u043f\u0440\u043e\u0441\u043b\u0443\u0448\u0438\u0432\u0430\u043d\u0438\u044f" + }, + "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Ecowitt?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ecowitt/translations/sv.json b/homeassistant/components/ecowitt/translations/sv.json new file mode 100644 index 00000000000..0edd1d70fa9 --- /dev/null +++ b/homeassistant/components/ecowitt/translations/sv.json @@ -0,0 +1,20 @@ +{ + "config": { + "create_entry": { + "default": "F\u00f6r att avsluta inst\u00e4llningen av integrationen, anv\u00e4nd Ecowitt-appen (p\u00e5 din telefon) eller g\u00e5 till Ecowitt WebUI i en webbl\u00e4sare p\u00e5 stationens IP-adress. \n\n V\u00e4lj din station - > Meny \u00d6vriga - > DIY Upload Servers. Klicka p\u00e5 n\u00e4sta och v\u00e4lj \"Anpassad\" \n\n - Server-IP: ` {server} `\n - S\u00f6kv\u00e4g: ` {path} `\n - Port: ` {port} ` \n\n Klicka p\u00e5 'Spara'." + }, + "error": { + "invalid_port": "Porten anv\u00e4nds redan.", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "data": { + "path": "S\u00f6kv\u00e4g med s\u00e4kerhetstoken", + "port": "Lyssningsport" + }, + "description": "\u00c4r du s\u00e4ker p\u00e5 att du vill konfigurera Ecowitt?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ecowitt/translations/tr.json b/homeassistant/components/ecowitt/translations/tr.json new file mode 100644 index 00000000000..8e4d6906e4b --- /dev/null +++ b/homeassistant/components/ecowitt/translations/tr.json @@ -0,0 +1,20 @@ +{ + "config": { + "create_entry": { + "default": "Entegrasyon kurulumunu tamamlamak i\u00e7in Ecowitt Uygulamas\u0131n\u0131 (telefonunuzda) kullan\u0131n veya istasyonun IP adresindeki bir taray\u0131c\u0131da Ecowitt WebUI'ye eri\u015fin. \n\n \u0130stasyonunuzu se\u00e7in - > Men\u00fc Di\u011ferleri - > Kendin Yap Y\u00fckleme Sunucular\u0131. \u0130leri'ye bas\u0131n ve '\u00d6zelle\u015ftirilmi\u015f'i se\u00e7in \n\n - Sunucu IP'si: ` {server} `\n - Yol: ` {path} `\n - Ba\u011flant\u0131 noktas\u0131: ` {port} ` \n\n 'Kaydet'e t\u0131klay\u0131n." + }, + "error": { + "invalid_port": "Ba\u011flant\u0131 noktas\u0131 zaten kullan\u0131l\u0131yor.", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "path": "G\u00fcvenlik anahtar\u0131 i\u00e7eren yol", + "port": "Dinleme ba\u011flant\u0131 noktas\u0131" + }, + "description": "Ecowitt'i kurmak istedi\u011finizden emin misiniz?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ecowitt/translations/zh-Hant.json b/homeassistant/components/ecowitt/translations/zh-Hant.json new file mode 100644 index 00000000000..3ad87a5733b --- /dev/null +++ b/homeassistant/components/ecowitt/translations/zh-Hant.json @@ -0,0 +1,20 @@ +{ + "config": { + "create_entry": { + "default": "\u5fc5\u9808\u57f7\u884c\u4ee5\u4e0b\u6b65\u9a5f\u4ee5\u8a2d\u5b9a\u6b64\u6574\u5408\u3001\u65bc\u624b\u6a5f\u4e0a\u4f7f\u7528 Ecowitt App \u6216\u4f7f\u7528\u700f\u89bd\u5668\u8f38\u5165\u7ad9\u9ede IP \u4f4d\u5740\u9032\u5165 Ecowitt WebUI\u3002\n\n\u9078\u64c7\u7ad9\u9ede -> \u9078\u55ae\u4e2d\u5176\u4ed6 -> DIY \u4e0a\u50b3\u4f3a\u670d\u5668\u3001\u9ede\u9078\u4e0b\u4e00\u6b65\u4e26\u9078\u64c7 '\u81ea\u8a02'\n\n- \u4f3a\u670d\u5668 IP\uff1a`{server}`\n- \u8def\u5f91\uff1a`{path}`\n- \u901a\u8a0a\u57e0\uff1a`{port}`\n\n\u9ede\u9078 '\u5132\u5b58'\u3002" + }, + "error": { + "invalid_port": "\u901a\u8a0a\u57e0\u5df2\u88ab\u4f7f\u7528\u3002", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "path": "\u52a0\u5bc6\u6b0a\u6756\u8def\u5f91", + "port": "\u76e3\u807d\u901a\u8a0a\u57e0" + }, + "description": "\u662f\u5426\u8981\u8a2d\u5b9a Ecowitt\uff1f" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/efergy/translations/es.json b/homeassistant/components/efergy/translations/es.json index a93f2f42235..0318f1b5837 100644 --- a/homeassistant/components/efergy/translations/es.json +++ b/homeassistant/components/efergy/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "error": { "cannot_connect": "No se pudo conectar", diff --git a/homeassistant/components/eight_sleep/translations/bg.json b/homeassistant/components/eight_sleep/translations/bg.json new file mode 100644 index 00000000000..31421ee3089 --- /dev/null +++ b/homeassistant/components/eight_sleep/translations/bg.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/elkm1/climate.py b/homeassistant/components/elkm1/climate.py index 6eca3083b3a..570c8567403 100644 --- a/homeassistant/components/elkm1/climate.py +++ b/homeassistant/components/elkm1/climate.py @@ -8,12 +8,12 @@ from elkm1_lib.elements import Element from elkm1_lib.elk import Elk from elkm1_lib.thermostats import Thermostat -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, FAN_AUTO, FAN_ON, + ClimateEntity, ClimateEntityFeature, HVACMode, ) diff --git a/homeassistant/components/elkm1/logbook.py b/homeassistant/components/elkm1/logbook.py index 9aa85b599e0..e86e58d23fd 100644 --- a/homeassistant/components/elkm1/logbook.py +++ b/homeassistant/components/elkm1/logbook.py @@ -3,10 +3,7 @@ from __future__ import annotations from collections.abc import Callable -from homeassistant.components.logbook.const import ( - LOGBOOK_ENTRY_MESSAGE, - LOGBOOK_ENTRY_NAME, -) +from homeassistant.components.logbook import LOGBOOK_ENTRY_MESSAGE, LOGBOOK_ENTRY_NAME from homeassistant.core import Event, HomeAssistant, callback from .const import ( diff --git a/homeassistant/components/elkm1/translations/cs.json b/homeassistant/components/elkm1/translations/cs.json index 9ff8da3067d..e371fb4ea98 100644 --- a/homeassistant/components/elkm1/translations/cs.json +++ b/homeassistant/components/elkm1/translations/cs.json @@ -4,7 +4,9 @@ "address_already_configured": "ElkM1 s touto adresou je ji\u017e nastaven", "already_configured": "ElkM1 s t\u00edmto prefixem je ji\u017e nastaven", "already_in_progress": "Konfigurace ji\u017e prob\u00edh\u00e1", - "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, "error": { "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", diff --git a/homeassistant/components/elmax/translations/cs.json b/homeassistant/components/elmax/translations/cs.json index 6c534c1a8a6..593ffb5f43c 100644 --- a/homeassistant/components/elmax/translations/cs.json +++ b/homeassistant/components/elmax/translations/cs.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + }, "error": { "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", "invalid_pin": "Poskytnut\u00fd k\u00f3d PIN je neplatn\u00fd", diff --git a/homeassistant/components/emby/media_player.py b/homeassistant/components/emby/media_player.py index 014f9e2ac1d..b573aef65ea 100644 --- a/homeassistant/components/emby/media_player.py +++ b/homeassistant/components/emby/media_player.py @@ -10,12 +10,8 @@ from homeassistant.components.media_player import ( PLATFORM_SCHEMA, MediaPlayerEntity, MediaPlayerEntityFeature, -) -from homeassistant.components.media_player.const import ( - MEDIA_TYPE_CHANNEL, - MEDIA_TYPE_MOVIE, - MEDIA_TYPE_MUSIC, - MEDIA_TYPE_TVSHOW, + MediaPlayerState, + MediaType, ) from homeassistant.const import ( CONF_API_KEY, @@ -25,10 +21,6 @@ from homeassistant.const import ( DEVICE_DEFAULT_NAME, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, - STATE_IDLE, - STATE_OFF, - STATE_PAUSED, - STATE_PLAYING, ) from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv @@ -39,7 +31,6 @@ import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) MEDIA_TYPE_TRAILER = "trailer" -MEDIA_TYPE_GENERIC_VIDEO = "video" DEFAULT_HOST = "localhost" DEFAULT_PORT = 8096 @@ -189,17 +180,18 @@ class EmbyDevice(MediaPlayerEntity): return f"Emby {self.device.name}" or DEVICE_DEFAULT_NAME @property - def state(self): + def state(self) -> MediaPlayerState | None: """Return the state of the device.""" state = self.device.state if state == "Paused": - return STATE_PAUSED + return MediaPlayerState.PAUSED if state == "Playing": - return STATE_PLAYING + return MediaPlayerState.PLAYING if state == "Idle": - return STATE_IDLE + return MediaPlayerState.IDLE if state == "Off": - return STATE_OFF + return MediaPlayerState.OFF + return None @property def app_name(self): @@ -213,23 +205,23 @@ class EmbyDevice(MediaPlayerEntity): return self.device.media_id @property - def media_content_type(self): + def media_content_type(self) -> MediaType | str | None: """Content type of current playing media.""" media_type = self.device.media_type if media_type == "Episode": - return MEDIA_TYPE_TVSHOW + return MediaType.TVSHOW if media_type == "Movie": - return MEDIA_TYPE_MOVIE + return MediaType.MOVIE if media_type == "Trailer": return MEDIA_TYPE_TRAILER if media_type == "Music": - return MEDIA_TYPE_MUSIC + return MediaType.MUSIC if media_type == "Video": - return MEDIA_TYPE_GENERIC_VIDEO + return MediaType.VIDEO if media_type == "Audio": - return MEDIA_TYPE_MUSIC + return MediaType.MUSIC if media_type == "TvChannel": - return MEDIA_TYPE_CHANNEL + return MediaType.CHANNEL return None @property diff --git a/homeassistant/components/emulated_hue/hue_api.py b/homeassistant/components/emulated_hue/hue_api.py index c5ff9654f90..272645909a5 100644 --- a/homeassistant/components/emulated_hue/hue_api.py +++ b/homeassistant/components/emulated_hue/hue_api.py @@ -11,6 +11,7 @@ import time from typing import Any from aiohttp import web +import async_timeout from homeassistant import core from homeassistant.components import ( @@ -23,7 +24,7 @@ from homeassistant.components import ( scene, script, ) -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( SERVICE_SET_TEMPERATURE, ClimateEntityFeature, ) @@ -34,10 +35,7 @@ from homeassistant.components.cover import ( ) from homeassistant.components.fan import ATTR_PERCENTAGE, FanEntityFeature from homeassistant.components.http import HomeAssistantView -from homeassistant.components.humidifier.const import ( - ATTR_HUMIDITY, - SERVICE_SET_HUMIDITY, -) +from homeassistant.components.humidifier import ATTR_HUMIDITY, SERVICE_SET_HUMIDITY from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, @@ -46,7 +44,7 @@ from homeassistant.components.light import ( ATTR_XY_COLOR, LightEntityFeature, ) -from homeassistant.components.media_player.const import ( +from homeassistant.components.media_player import ( ATTR_MEDIA_VOLUME_LEVEL, MediaPlayerEntityFeature, ) @@ -874,7 +872,8 @@ async def wait_for_state_change_or_timeout( unsub = async_track_state_change_event(hass, [entity_id], _async_event_changed) try: - await asyncio.wait_for(ev.wait(), timeout=STATE_CHANGE_WAIT_TIMEOUT) + async with async_timeout.timeout(STATE_CHANGE_WAIT_TIMEOUT): + await ev.wait() except asyncio.TimeoutError: pass finally: diff --git a/homeassistant/components/energy/data.py b/homeassistant/components/energy/data.py index 3b3952136be..bc7903203c4 100644 --- a/homeassistant/components/energy/data.py +++ b/homeassistant/components/energy/data.py @@ -32,12 +32,11 @@ class FlowFromGridSourceType(TypedDict): stat_energy_from: str # statistic_id of costs ($) incurred from the energy meter - # If set to None and entity_energy_from and entity_energy_price are configured, + # If set to None and entity_energy_price or number_energy_price are configured, # an EnergyCostSensor will be automatically created stat_cost: str | None # Used to generate costs if stat_cost is set to None - entity_energy_from: str | None # entity_id of an energy meter (kWh), entity_id of the energy meter for stat_energy_from entity_energy_price: str | None # entity_id of an entity providing price ($/kWh) number_energy_price: float | None # Price for energy ($/kWh) @@ -49,12 +48,11 @@ class FlowToGridSourceType(TypedDict): stat_energy_to: str # statistic_id of compensation ($) received for contributing back - # If set to None and entity_energy_from and entity_energy_price are configured, + # If set to None and entity_energy_price or number_energy_price are configured, # an EnergyCostSensor will be automatically created stat_compensation: str | None # Used to generate costs if stat_compensation is set to None - entity_energy_to: str | None # entity_id of an energy meter (kWh), entity_id of the energy meter for stat_energy_to entity_energy_price: str | None # entity_id of an entity providing price ($/kWh) number_energy_price: float | None # Price for energy ($/kWh) @@ -96,12 +94,11 @@ class GasSourceType(TypedDict): stat_energy_from: str # statistic_id of costs ($) incurred from the energy meter - # If set to None and entity_energy_from and entity_energy_price are configured, + # If set to None and entity_energy_price or number_energy_price are configured, # an EnergyCostSensor will be automatically created stat_cost: str | None # Used to generate costs if stat_cost is set to None - entity_energy_from: str | None # entity_id of an gas meter (m³), entity_id of the gas meter for stat_energy_from entity_energy_price: str | None # entity_id of an entity providing price ($/m³) number_energy_price: float | None # Price for energy ($/m³) @@ -145,7 +142,8 @@ FLOW_FROM_GRID_SOURCE_SCHEMA = vol.All( { vol.Required("stat_energy_from"): str, vol.Optional("stat_cost"): vol.Any(str, None), - vol.Optional("entity_energy_from"): vol.Any(str, None), + # entity_energy_from was removed in HA Core 2022.10 + vol.Remove("entity_energy_from"): vol.Any(str, None), vol.Optional("entity_energy_price"): vol.Any(str, None), vol.Optional("number_energy_price"): vol.Any(vol.Coerce(float), None), } @@ -158,7 +156,8 @@ FLOW_TO_GRID_SOURCE_SCHEMA = vol.Schema( { vol.Required("stat_energy_to"): str, vol.Optional("stat_compensation"): vol.Any(str, None), - vol.Optional("entity_energy_to"): vol.Any(str, None), + # entity_energy_to was removed in HA Core 2022.10 + vol.Remove("entity_energy_to"): vol.Any(str, None), vol.Optional("entity_energy_price"): vol.Any(str, None), vol.Optional("number_energy_price"): vol.Any(vol.Coerce(float), None), } @@ -216,7 +215,8 @@ GAS_SOURCE_SCHEMA = vol.Schema( vol.Required("type"): "gas", vol.Required("stat_energy_from"): str, vol.Optional("stat_cost"): vol.Any(str, None), - vol.Optional("entity_energy_from"): vol.Any(str, None), + # entity_energy_from was removed in HA Core 2022.10 + vol.Remove("entity_energy_from"): vol.Any(str, None), vol.Optional("entity_energy_price"): vol.Any(str, None), vol.Optional("number_energy_price"): vol.Any(vol.Coerce(float), None), } diff --git a/homeassistant/components/energy/manifest.json b/homeassistant/components/energy/manifest.json index 5ddc6457a61..39a3f66d65c 100644 --- a/homeassistant/components/energy/manifest.json +++ b/homeassistant/components/energy/manifest.json @@ -5,5 +5,6 @@ "codeowners": ["@home-assistant/core"], "iot_class": "calculated", "dependencies": ["websocket_api", "history", "recorder"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "system" } diff --git a/homeassistant/components/energy/sensor.py b/homeassistant/components/energy/sensor.py index 602fc09f602..db156a2d6cc 100644 --- a/homeassistant/components/energy/sensor.py +++ b/homeassistant/components/energy/sensor.py @@ -67,7 +67,6 @@ class SourceAdapter: source_type: Literal["grid", "gas"] flow_type: Literal["flow_from", "flow_to", None] stat_energy_key: Literal["stat_energy_from", "stat_energy_to"] - entity_energy_key: Literal["entity_energy_from", "entity_energy_to"] total_money_key: Literal["stat_cost", "stat_compensation"] name_suffix: str entity_id_suffix: str @@ -78,7 +77,6 @@ SOURCE_ADAPTERS: Final = ( "grid", "flow_from", "stat_energy_from", - "entity_energy_from", "stat_cost", "Cost", "cost", @@ -87,7 +85,6 @@ SOURCE_ADAPTERS: Final = ( "grid", "flow_to", "stat_energy_to", - "entity_energy_to", "stat_compensation", "Compensation", "compensation", @@ -96,7 +93,6 @@ SOURCE_ADAPTERS: Final = ( "gas", None, "stat_energy_from", - "entity_energy_from", "stat_cost", "Cost", "cost", @@ -183,13 +179,9 @@ class SensorManager: # Make sure the right data is there # If the entity existed, we don't pop it from to_remove so it's removed - if ( - config.get(adapter.entity_energy_key) is None - or not valid_entity_id(config[adapter.entity_energy_key]) - or ( - config.get("entity_energy_price") is None - and config.get("number_energy_price") is None - ) + if not valid_entity_id(config[adapter.stat_energy_key]) or ( + config.get("entity_energy_price") is None + and config.get("number_energy_price") is None ): return @@ -224,9 +216,7 @@ class EnergyCostSensor(SensorEntity): super().__init__() self._adapter = adapter - self.entity_id = ( - f"{config[adapter.entity_energy_key]}_{adapter.entity_id_suffix}" - ) + self.entity_id = f"{config[adapter.stat_energy_key]}_{adapter.entity_id_suffix}" self._attr_device_class = SensorDeviceClass.MONETARY self._attr_state_class = SensorStateClass.TOTAL self._config = config @@ -246,7 +236,7 @@ class EnergyCostSensor(SensorEntity): def _update_cost(self) -> None: """Update incurred costs.""" energy_state = self.hass.states.get( - cast(str, self._config[self._adapter.entity_energy_key]) + cast(str, self._config[self._adapter.stat_energy_key]) ) if energy_state is None: @@ -344,7 +334,7 @@ class EnergyCostSensor(SensorEntity): self._reset(energy_state_copy) elif state_class == SensorStateClass.TOTAL_INCREASING and reset_detected( self.hass, - cast(str, self._config[self._adapter.entity_energy_key]), + cast(str, self._config[self._adapter.stat_energy_key]), energy, float(self._last_energy_sensor_state.state), self._last_energy_sensor_state, @@ -362,13 +352,11 @@ class EnergyCostSensor(SensorEntity): async def async_added_to_hass(self) -> None: """Register callbacks.""" - energy_state = self.hass.states.get( - self._config[self._adapter.entity_energy_key] - ) + energy_state = self.hass.states.get(self._config[self._adapter.stat_energy_key]) if energy_state: name = energy_state.name else: - name = split_entity_id(self._config[self._adapter.entity_energy_key])[ + name = split_entity_id(self._config[self._adapter.stat_energy_key])[ 0 ].replace("_", " ") @@ -378,7 +366,7 @@ class EnergyCostSensor(SensorEntity): # Store stat ID in hass.data so frontend can look it up self.hass.data[DOMAIN]["cost_sensors"][ - self._config[self._adapter.entity_energy_key] + self._config[self._adapter.stat_energy_key] ] = self.entity_id @callback @@ -390,7 +378,7 @@ class EnergyCostSensor(SensorEntity): self.async_on_remove( async_track_state_change_event( self.hass, - cast(str, self._config[self._adapter.entity_energy_key]), + cast(str, self._config[self._adapter.stat_energy_key]), async_state_changed_listener, ) ) @@ -404,7 +392,7 @@ class EnergyCostSensor(SensorEntity): async def async_will_remove_from_hass(self) -> None: """Handle removing from hass.""" self.hass.data[DOMAIN]["cost_sensors"].pop( - self._config[self._adapter.entity_energy_key] + self._config[self._adapter.stat_energy_key] ) await super().async_will_remove_from_hass() @@ -423,10 +411,10 @@ class EnergyCostSensor(SensorEntity): """Return the unique ID of the sensor.""" entity_registry = er.async_get(self.hass) if registry_entry := entity_registry.async_get( - self._config[self._adapter.entity_energy_key] + self._config[self._adapter.stat_energy_key] ): prefix = registry_entry.id else: - prefix = self._config[self._adapter.entity_energy_key] + prefix = self._config[self._adapter.stat_energy_key] return f"{prefix}_{self._adapter.source_type}_{self._adapter.entity_id_suffix}" diff --git a/homeassistant/components/energy/validate.py b/homeassistant/components/energy/validate.py index 9d6b3bd53c7..b704330fa28 100644 --- a/homeassistant/components/energy/validate.py +++ b/homeassistant/components/energy/validate.py @@ -322,7 +322,7 @@ async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation: ) ) - if flow.get("entity_energy_from") is not None and ( + if ( flow.get("entity_energy_price") is not None or flow.get("number_energy_price") is not None ): @@ -330,7 +330,7 @@ async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation: functools.partial( _async_validate_auto_generated_cost_entity, hass, - flow["entity_energy_from"], + flow["stat_energy_from"], source_result, ) ) @@ -373,7 +373,7 @@ async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation: ) ) - if flow.get("entity_energy_to") is not None and ( + if ( flow.get("entity_energy_price") is not None or flow.get("number_energy_price") is not None ): @@ -381,7 +381,7 @@ async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation: functools.partial( _async_validate_auto_generated_cost_entity, hass, - flow["entity_energy_to"], + flow["stat_energy_to"], source_result, ) ) @@ -424,7 +424,7 @@ async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation: ) ) - if source.get("entity_energy_from") is not None and ( + if ( source.get("entity_energy_price") is not None or source.get("number_energy_price") is not None ): @@ -432,7 +432,7 @@ async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation: functools.partial( _async_validate_auto_generated_cost_entity, hass, - source["entity_energy_from"], + source["stat_energy_from"], source_result, ) ) diff --git a/homeassistant/components/energy/websocket_api.py b/homeassistant/components/energy/websocket_api.py index ad77308b410..7ba83cf15c9 100644 --- a/homeassistant/components/energy/websocket_api.py +++ b/homeassistant/components/energy/websocket_api.py @@ -13,6 +13,7 @@ from typing import Any, cast import voluptuous as vol from homeassistant.components import recorder, websocket_api +from homeassistant.const import ENERGY_KILO_WATT_HOUR from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.integration_platform import ( async_process_integration_platforms, @@ -268,6 +269,7 @@ async def ws_get_fossil_energy_consumption( statistic_ids, "hour", True, + {"energy": ENERGY_KILO_WATT_HOUR}, ) def _combine_sum_statistics( diff --git a/homeassistant/components/enigma2/media_player.py b/homeassistant/components/enigma2/media_player.py index aab3514b8e0..a479590f464 100644 --- a/homeassistant/components/enigma2/media_player.py +++ b/homeassistant/components/enigma2/media_player.py @@ -7,8 +7,9 @@ import voluptuous as vol from homeassistant.components.media_player import ( MediaPlayerEntity, MediaPlayerEntityFeature, + MediaPlayerState, + MediaType, ) -from homeassistant.components.media_player.const import MEDIA_TYPE_TVSHOW from homeassistant.const import ( CONF_HOST, CONF_NAME, @@ -16,9 +17,6 @@ from homeassistant.const import ( CONF_PORT, CONF_SSL, CONF_USERNAME, - STATE_OFF, - STATE_ON, - STATE_PLAYING, ) from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -104,6 +102,7 @@ def setup_platform( class Enigma2Device(MediaPlayerEntity): """Representation of an Enigma2 box.""" + _attr_media_content_type = MediaType.TVSHOW _attr_supported_features = ( MediaPlayerEntityFeature.VOLUME_SET | MediaPlayerEntityFeature.VOLUME_MUTE @@ -133,11 +132,11 @@ class Enigma2Device(MediaPlayerEntity): return self.e2_box.mac_address @property - def state(self): + def state(self) -> MediaPlayerState: """Return the state of the device.""" if self.e2_box.is_recording_playback: - return STATE_PLAYING - return STATE_OFF if self.e2_box.in_standby else STATE_ON + return MediaPlayerState.PLAYING + return MediaPlayerState.OFF if self.e2_box.in_standby else MediaPlayerState.ON @property def available(self) -> bool: @@ -172,11 +171,6 @@ class Enigma2Device(MediaPlayerEntity): """Service Ref of current playing media.""" return self.e2_box.current_service_ref - @property - def media_content_type(self): - """Type of video currently playing.""" - return MEDIA_TYPE_TVSHOW - @property def is_volume_muted(self): """Boolean if volume is currently muted.""" diff --git a/homeassistant/components/enphase_envoy/translations/es.json b/homeassistant/components/enphase_envoy/translations/es.json index ab385d0a282..c30865d6ffb 100644 --- a/homeassistant/components/enphase_envoy/translations/es.json +++ b/homeassistant/components/enphase_envoy/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "error": { "cannot_connect": "No se pudo conectar", diff --git a/homeassistant/components/environment_canada/sensor.py b/homeassistant/components/environment_canada/sensor.py index 08da60fe01f..88ec055ad03 100644 --- a/homeassistant/components/environment_canada/sensor.py +++ b/homeassistant/components/environment_canada/sensor.py @@ -172,6 +172,7 @@ SENSOR_TYPES: tuple[ECSensorEntityDescription, ...] = ( key="visibility", name="Visibility", native_unit_of_measurement=LENGTH_KILOMETERS, + device_class=SensorDeviceClass.DISTANCE, state_class=SensorStateClass.MEASUREMENT, value_fn=lambda data: data.conditions.get("visibility", {}).get("value"), ), @@ -198,6 +199,7 @@ SENSOR_TYPES: tuple[ECSensorEntityDescription, ...] = ( key="wind_gust", name="Wind gust", native_unit_of_measurement=SPEED_KILOMETERS_PER_HOUR, + device_class=SensorDeviceClass.SPEED, state_class=SensorStateClass.MEASUREMENT, value_fn=lambda data: data.conditions.get("wind_gust", {}).get("value"), ), @@ -205,6 +207,7 @@ SENSOR_TYPES: tuple[ECSensorEntityDescription, ...] = ( key="wind_speed", name="Wind speed", native_unit_of_measurement=SPEED_KILOMETERS_PER_HOUR, + device_class=SensorDeviceClass.SPEED, state_class=SensorStateClass.MEASUREMENT, value_fn=lambda data: data.conditions.get("wind_speed", {}).get("value"), ), diff --git a/homeassistant/components/ephember/climate.py b/homeassistant/components/ephember/climate.py index f308e116ed6..9c83a1c8a67 100644 --- a/homeassistant/components/ephember/climate.py +++ b/homeassistant/components/ephember/climate.py @@ -18,8 +18,9 @@ from pyephember.pyephember import ( ) import voluptuous as vol -from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( + PLATFORM_SCHEMA, + ClimateEntity, ClimateEntityFeature, HVACAction, HVACMode, diff --git a/homeassistant/components/epson/media_player.py b/homeassistant/components/epson/media_player.py index 0e70984ac31..a0d1476aea7 100644 --- a/homeassistant/components/epson/media_player.py +++ b/homeassistant/components/epson/media_player.py @@ -31,9 +31,9 @@ import voluptuous as vol from homeassistant.components.media_player import ( MediaPlayerEntity, MediaPlayerEntityFeature, + MediaPlayerState, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_platform import homeassistant.helpers.config_validation as cv @@ -134,7 +134,7 @@ class EpsonProjectorMediaPlayer(MediaPlayerEntity): _LOGGER.debug("Projector status: %s", power_state) self._attr_available = True if power_state == EPSON_CODES[POWER]: - self._attr_state = STATE_ON + self._attr_state = MediaPlayerState.ON if await self.set_unique_id(): return self._attr_source_list = list(DEFAULT_SOURCES.values()) @@ -148,21 +148,21 @@ class EpsonProjectorMediaPlayer(MediaPlayerEntity): except ValueError: self._attr_volume_level = None elif power_state == BUSY: - self._attr_state = STATE_ON + self._attr_state = MediaPlayerState.ON else: - self._attr_state = STATE_OFF + self._attr_state = MediaPlayerState.OFF async def async_turn_on(self) -> None: """Turn on epson.""" - if self.state == STATE_OFF: + if self.state == MediaPlayerState.OFF: await self._projector.send_command(TURN_ON) - self._attr_state = STATE_ON + self._attr_state = MediaPlayerState.ON async def async_turn_off(self) -> None: """Turn off epson.""" - if self.state == STATE_ON: + if self.state == MediaPlayerState.ON: await self._projector.send_command(TURN_OFF) - self._attr_state = STATE_OFF + self._attr_state = MediaPlayerState.OFF async def select_cmode(self, cmode: str) -> None: """Set color mode in Epson.""" diff --git a/homeassistant/components/eq3btsmart/climate.py b/homeassistant/components/eq3btsmart/climate.py index e469512123b..027366c96ef 100644 --- a/homeassistant/components/eq3btsmart/climate.py +++ b/homeassistant/components/eq3btsmart/climate.py @@ -7,11 +7,12 @@ from typing import Any import eq3bt as eq3 # pylint: disable=import-error import voluptuous as vol -from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( + PLATFORM_SCHEMA, PRESET_AWAY, PRESET_BOOST, PRESET_NONE, + ClimateEntity, ClimateEntityFeature, HVACMode, ) diff --git a/homeassistant/components/escea/climate.py b/homeassistant/components/escea/climate.py index 1ddea9cb026..a764c019c50 100644 --- a/homeassistant/components/escea/climate.py +++ b/homeassistant/components/escea/climate.py @@ -7,11 +7,11 @@ from typing import Any from pescea import Controller -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( FAN_AUTO, FAN_HIGH, FAN_LOW, + ClimateEntity, ClimateEntityFeature, HVACMode, ) diff --git a/homeassistant/components/escea/translations/bg.json b/homeassistant/components/escea/translations/bg.json new file mode 100644 index 00000000000..ed4c38dba5a --- /dev/null +++ b/homeassistant/components/escea/translations/bg.json @@ -0,0 +1,8 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u041d\u044f\u043c\u0430 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0432 \u043c\u0440\u0435\u0436\u0430\u0442\u0430", + "single_instance_allowed": "\u0412\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e. \u0412\u044a\u0437\u043c\u043e\u0436\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/escea/translations/cs.json b/homeassistant/components/escea/translations/cs.json new file mode 100644 index 00000000000..d2e2907679e --- /dev/null +++ b/homeassistant/components/escea/translations/cs.json @@ -0,0 +1,8 @@ +{ + "config": { + "abort": { + "no_devices_found": "V s\u00edti nebyla nalezena \u017e\u00e1dn\u00e1 za\u0159\u00edzen\u00ed", + "single_instance_allowed": "Ji\u017e nastaveno. Je mo\u017en\u00e1 pouze jedin\u00e1 konfigurace." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/escea/translations/pt.json b/homeassistant/components/escea/translations/pt.json new file mode 100644 index 00000000000..3650c239009 --- /dev/null +++ b/homeassistant/components/escea/translations/pt.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "confirm": { + "description": "Quer montar uma lareira Escea?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/escea/translations/sv.json b/homeassistant/components/escea/translations/sv.json new file mode 100644 index 00000000000..fca3faef5f4 --- /dev/null +++ b/homeassistant/components/escea/translations/sv.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "Inga enheter hittades i n\u00e4tverket", + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." + }, + "step": { + "confirm": { + "description": "Vill du s\u00e4tta upp en Escea eldstad?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index 07b6d3071f6..8846007374e 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -2,7 +2,6 @@ from __future__ import annotations from collections.abc import Callable -from dataclasses import dataclass, field import functools import logging import math @@ -47,70 +46,18 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo, Entity, EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_state_change_event -from homeassistant.helpers.json import JSONEncoder from homeassistant.helpers.service import async_set_service_schema -from homeassistant.helpers.storage import Store from homeassistant.helpers.template import Template from .bluetooth import async_connect_scanner +from .domain_data import DOMAIN, DomainData # Import config flow so that it's added to the registry from .entry_data import RuntimeEntryData -DOMAIN = "esphome" CONF_NOISE_PSK = "noise_psk" _LOGGER = logging.getLogger(__name__) _R = TypeVar("_R") -_DomainDataSelfT = TypeVar("_DomainDataSelfT", bound="DomainData") - -STORAGE_VERSION = 1 - - -@dataclass -class DomainData: - """Define a class that stores global esphome data in hass.data[DOMAIN].""" - - _entry_datas: dict[str, RuntimeEntryData] = field(default_factory=dict) - _stores: dict[str, Store] = field(default_factory=dict) - - def get_entry_data(self, entry: ConfigEntry) -> RuntimeEntryData: - """Return the runtime entry data associated with this config entry. - - Raises KeyError if the entry isn't loaded yet. - """ - return self._entry_datas[entry.entry_id] - - def set_entry_data(self, entry: ConfigEntry, entry_data: RuntimeEntryData) -> None: - """Set the runtime entry data associated with this config entry.""" - if entry.entry_id in self._entry_datas: - raise ValueError("Entry data for this entry is already set") - self._entry_datas[entry.entry_id] = entry_data - - def pop_entry_data(self, entry: ConfigEntry) -> RuntimeEntryData: - """Pop the runtime entry data instance associated with this config entry.""" - return self._entry_datas.pop(entry.entry_id) - - def is_entry_loaded(self, entry: ConfigEntry) -> bool: - """Check whether the given entry is loaded.""" - return entry.entry_id in self._entry_datas - - def get_or_create_store(self, hass: HomeAssistant, entry: ConfigEntry) -> Store: - """Get or create a Store instance for the given config entry.""" - return self._stores.setdefault( - entry.entry_id, - Store( - hass, STORAGE_VERSION, f"esphome.{entry.entry_id}", encoder=JSONEncoder - ), - ) - - @classmethod - def get(cls: type[_DomainDataSelfT], hass: HomeAssistant) -> _DomainDataSelfT: - """Get the global DomainData instance stored in hass.data.""" - # Don't use setdefault - this is a hot code path - if DOMAIN in hass.data: - return cast(_DomainDataSelfT, hass.data[DOMAIN]) - ret = hass.data[DOMAIN] = cls() - return ret async def async_setup_entry( # noqa: C901 @@ -289,8 +236,10 @@ async def async_setup_entry( # noqa: C901 await cli.subscribe_states(entry_data.async_update_state) await cli.subscribe_service_calls(async_on_service_call) await cli.subscribe_home_assistant_states(async_on_state_subscription) - if entry_data.device_info.has_bluetooth_proxy: - await async_connect_scanner(hass, entry, cli) + if entry_data.device_info.bluetooth_proxy_version: + entry_data.disconnect_callbacks.append( + await async_connect_scanner(hass, entry, cli, entry_data) + ) hass.async_create_task(entry_data.async_save_to_store()) except APIConnectionError as err: diff --git a/homeassistant/components/esphome/bluetooth/__init__.py b/homeassistant/components/esphome/bluetooth/__init__.py new file mode 100644 index 00000000000..4f3235676a4 --- /dev/null +++ b/homeassistant/components/esphome/bluetooth/__init__.py @@ -0,0 +1,84 @@ +"""Bluetooth support for esphome.""" +from __future__ import annotations + +import logging + +from aioesphomeapi import APIClient + +from homeassistant.components.bluetooth import ( + HaBluetoothConnector, + async_get_advertisement_callback, + async_register_scanner, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import ( + CALLBACK_TYPE, + HomeAssistant, + async_get_hass, + callback as hass_callback, +) + +from ..domain_data import DomainData +from ..entry_data import RuntimeEntryData +from .client import ESPHomeClient +from .scanner import ESPHomeScanner + +_LOGGER = logging.getLogger(__name__) + + +@hass_callback +def async_can_connect(source: str) -> bool: + """Check if a given source can make another connection.""" + domain_data = DomainData.get(async_get_hass()) + entry = domain_data.get_by_unique_id(source) + entry_data = domain_data.get_entry_data(entry) + _LOGGER.debug( + "Checking if %s can connect, available=%s, ble_connections_free=%s", + source, + entry_data.available, + entry_data.ble_connections_free, + ) + return bool(entry_data.available and entry_data.ble_connections_free) + + +async def async_connect_scanner( + hass: HomeAssistant, + entry: ConfigEntry, + cli: APIClient, + entry_data: RuntimeEntryData, +) -> CALLBACK_TYPE: + """Connect scanner.""" + assert entry.unique_id is not None + source = str(entry.unique_id) + new_info_callback = async_get_advertisement_callback(hass) + assert entry_data.device_info is not None + version = entry_data.device_info.bluetooth_proxy_version + connectable = version >= 2 + _LOGGER.debug( + "Connecting scanner for %s, version=%s, connectable=%s", + source, + version, + connectable, + ) + connector = HaBluetoothConnector( + client=ESPHomeClient, + source=source, + can_connect=lambda: async_can_connect(source), + ) + scanner = ESPHomeScanner(hass, source, new_info_callback, connector, connectable) + unload_callbacks = [ + async_register_scanner(hass, scanner, connectable), + scanner.async_setup(), + ] + await cli.subscribe_bluetooth_le_advertisements(scanner.async_on_advertisement) + if connectable: + await cli.subscribe_bluetooth_connections_free( + entry_data.async_update_ble_connection_limits + ) + + @hass_callback + def _async_unload() -> None: + for callback in unload_callbacks: + callback() + + return _async_unload diff --git a/homeassistant/components/esphome/bluetooth/characteristic.py b/homeassistant/components/esphome/bluetooth/characteristic.py new file mode 100644 index 00000000000..0db73dd3d5f --- /dev/null +++ b/homeassistant/components/esphome/bluetooth/characteristic.py @@ -0,0 +1,95 @@ +"""BleakGATTCharacteristicESPHome.""" +from __future__ import annotations + +import contextlib +from uuid import UUID + +from aioesphomeapi.model import BluetoothGATTCharacteristic +from bleak.backends.characteristic import BleakGATTCharacteristic +from bleak.backends.descriptor import BleakGATTDescriptor + +PROPERTY_MASKS = { + 2**n: prop + for n, prop in enumerate( + ( + "broadcast", + "read", + "write-without-response", + "write", + "notify", + "indicate", + "authenticated-signed-writes", + "extended-properties", + "reliable-writes", + "writable-auxiliaries", + ) + ) +} + + +class BleakGATTCharacteristicESPHome(BleakGATTCharacteristic): + """GATT Characteristic implementation for the ESPHome backend.""" + + obj: BluetoothGATTCharacteristic + + def __init__( + self, + obj: BluetoothGATTCharacteristic, + max_write_without_response_size: int, + service_uuid: str, + service_handle: int, + ) -> None: + """Init a BleakGATTCharacteristicESPHome.""" + super().__init__(obj, max_write_without_response_size) + self.__descriptors: list[BleakGATTDescriptor] = [] + self.__service_uuid: str = service_uuid + self.__service_handle: int = service_handle + char_props = self.obj.properties + self.__props: list[str] = [ + prop for mask, prop in PROPERTY_MASKS.items() if char_props & mask + ] + + @property + def service_uuid(self) -> str: + """Uuid of the Service containing this characteristic.""" + return self.__service_uuid + + @property + def service_handle(self) -> int: + """Integer handle of the Service containing this characteristic.""" + return self.__service_handle + + @property + def handle(self) -> int: + """Integer handle for this characteristic.""" + return self.obj.handle + + @property + def uuid(self) -> str: + """Uuid of this characteristic.""" + return self.obj.uuid + + @property + def properties(self) -> list[str]: + """Properties of this characteristic.""" + return self.__props + + @property + def descriptors(self) -> list[BleakGATTDescriptor]: + """List of descriptors for this service.""" + return self.__descriptors + + def get_descriptor(self, specifier: int | str | UUID) -> BleakGATTDescriptor | None: + """Get a descriptor by handle (int) or UUID (str or uuid.UUID).""" + with contextlib.suppress(StopIteration): + if isinstance(specifier, int): + return next(filter(lambda x: x.handle == specifier, self.descriptors)) + return next(filter(lambda x: x.uuid == str(specifier), self.descriptors)) + return None + + def add_descriptor(self, descriptor: BleakGATTDescriptor) -> None: + """Add a :py:class:`~BleakGATTDescriptor` to the characteristic. + + Should not be used by end user, but rather by `bleak` itself. + """ + self.__descriptors.append(descriptor) diff --git a/homeassistant/components/esphome/bluetooth/client.py b/homeassistant/components/esphome/bluetooth/client.py new file mode 100644 index 00000000000..14924756074 --- /dev/null +++ b/homeassistant/components/esphome/bluetooth/client.py @@ -0,0 +1,415 @@ +"""Bluetooth client for esphome.""" +from __future__ import annotations + +import asyncio +from collections.abc import Callable, Coroutine +import logging +from typing import Any, TypeVar, cast +import uuid + +from aioesphomeapi.connection import APIConnectionError, TimeoutAPIError +import async_timeout +from bleak.backends.characteristic import BleakGATTCharacteristic +from bleak.backends.client import BaseBleakClient, NotifyCallback +from bleak.backends.device import BLEDevice +from bleak.backends.service import BleakGATTServiceCollection +from bleak.exc import BleakError + +from homeassistant.core import CALLBACK_TYPE, async_get_hass + +from ..domain_data import DomainData +from .characteristic import BleakGATTCharacteristicESPHome +from .descriptor import BleakGATTDescriptorESPHome +from .service import BleakGATTServiceESPHome + +DEFAULT_MTU = 23 +GATT_HEADER_SIZE = 3 +DISCONNECT_TIMEOUT = 5.0 +CONNECT_FREE_SLOT_TIMEOUT = 2.0 +GATT_READ_TIMEOUT = 30.0 + +DEFAULT_MAX_WRITE_WITHOUT_RESPONSE = DEFAULT_MTU - GATT_HEADER_SIZE +_LOGGER = logging.getLogger(__name__) + +_WrapFuncType = TypeVar( # pylint: disable=invalid-name + "_WrapFuncType", bound=Callable[..., Any] +) + + +def mac_to_int(address: str) -> int: + """Convert a mac address to an integer.""" + return int(address.replace(":", ""), 16) + + +def verify_connected(func: _WrapFuncType) -> _WrapFuncType: + """Define a wrapper throw BleakError if not connected.""" + + async def _async_wrap_bluetooth_connected_operation( + self: "ESPHomeClient", *args: Any, **kwargs: Any + ) -> Any: + if not self._is_connected: # pylint: disable=protected-access + raise BleakError("Not connected") + return await func(self, *args, **kwargs) + + return cast(_WrapFuncType, _async_wrap_bluetooth_connected_operation) + + +def api_error_as_bleak_error(func: _WrapFuncType) -> _WrapFuncType: + """Define a wrapper throw esphome api errors as BleakErrors.""" + + async def _async_wrap_bluetooth_operation( + self: "ESPHomeClient", *args: Any, **kwargs: Any + ) -> Any: + try: + return await func(self, *args, **kwargs) + except TimeoutAPIError as err: + raise asyncio.TimeoutError(str(err)) from err + except APIConnectionError as err: + raise BleakError(str(err)) from err + + return cast(_WrapFuncType, _async_wrap_bluetooth_operation) + + +class ESPHomeClient(BaseBleakClient): + """ESPHome Bleak Client.""" + + def __init__( + self, address_or_ble_device: BLEDevice | str, *args: Any, **kwargs: Any + ) -> None: + """Initialize the ESPHomeClient.""" + assert isinstance(address_or_ble_device, BLEDevice) + super().__init__(address_or_ble_device, *args, **kwargs) + self._ble_device = address_or_ble_device + self._address_as_int = mac_to_int(self._ble_device.address) + assert self._ble_device.details is not None + self._source = self._ble_device.details["source"] + self.domain_data = DomainData.get(async_get_hass()) + config_entry = self.domain_data.get_by_unique_id(self._source) + self.entry_data = self.domain_data.get_entry_data(config_entry) + self._client = self.entry_data.client + self._is_connected = False + self._mtu: int | None = None + self._cancel_connection_state: CALLBACK_TYPE | None = None + self._notify_cancels: dict[int, Callable[[], Coroutine[Any, Any, None]]] = {} + + def __str__(self) -> str: + """Return the string representation of the client.""" + return f"ESPHomeClient ({self.address})" + + def _unsubscribe_connection_state(self) -> None: + """Unsubscribe from connection state updates.""" + if not self._cancel_connection_state: + return + try: + self._cancel_connection_state() + except (AssertionError, ValueError) as ex: + _LOGGER.debug( + "Failed to unsubscribe from connection state (likely connection dropped): %s", + ex, + ) + self._cancel_connection_state = None + + def _async_ble_device_disconnected(self) -> None: + """Handle the BLE device disconnecting from the ESP.""" + _LOGGER.debug("%s: BLE device disconnected", self._source) + self._is_connected = False + self.services = BleakGATTServiceCollection() # type: ignore[no-untyped-call] + self._async_call_bleak_disconnected_callback() + self._unsubscribe_connection_state() + + def _async_esp_disconnected(self) -> None: + """Handle the esp32 client disconnecting from hass.""" + _LOGGER.debug("%s: ESP device disconnected", self._source) + self.entry_data.disconnect_callbacks.remove(self._async_esp_disconnected) + self._async_ble_device_disconnected() + + def _async_call_bleak_disconnected_callback(self) -> None: + """Call the disconnected callback to inform the bleak consumer.""" + if self._disconnected_callback: + self._disconnected_callback(self) + self._disconnected_callback = None + + @api_error_as_bleak_error + async def connect( + self, dangerous_use_bleak_cache: bool = False, **kwargs: Any + ) -> bool: + """Connect to a specified Peripheral. + + Keyword Args: + timeout (float): Timeout for required ``BleakScanner.find_device_by_address`` call. Defaults to 10.0. + Returns: + Boolean representing connection status. + """ + await self._wait_for_free_connection_slot(CONNECT_FREE_SLOT_TIMEOUT) + + connected_future: asyncio.Future[bool] = asyncio.Future() + + def _on_bluetooth_connection_state( + connected: bool, mtu: int, error: int + ) -> None: + """Handle a connect or disconnect.""" + _LOGGER.debug( + "Connection state changed: connected=%s mtu=%s error=%s", + connected, + mtu, + error, + ) + if connected: + self._is_connected = True + self._mtu = mtu + else: + self._async_ble_device_disconnected() + + if connected_future.done(): + return + + if error: + connected_future.set_exception( + BleakError(f"Error while connecting: {error}") + ) + return + + if not connected: + connected_future.set_exception(BleakError("Disconnected")) + return + + self.entry_data.disconnect_callbacks.append(self._async_esp_disconnected) + connected_future.set_result(connected) + + timeout = kwargs.get("timeout", self._timeout) + self._cancel_connection_state = await self._client.bluetooth_device_connect( + self._address_as_int, + _on_bluetooth_connection_state, + timeout=timeout, + ) + await connected_future + await self.get_services(dangerous_use_bleak_cache=dangerous_use_bleak_cache) + return True + + @api_error_as_bleak_error + async def disconnect(self) -> bool: + """Disconnect from the peripheral device.""" + self._unsubscribe_connection_state() + await self._client.bluetooth_device_disconnect(self._address_as_int) + await self._wait_for_free_connection_slot(DISCONNECT_TIMEOUT) + return True + + async def _wait_for_free_connection_slot(self, timeout: float) -> None: + """Wait for a free connection slot.""" + if self.entry_data.ble_connections_free: + return + _LOGGER.debug( + "%s: Out of connection slots, waiting for a free one", self._source + ) + async with async_timeout.timeout(timeout): + await self.entry_data.wait_for_ble_connections_free() + + @property + def is_connected(self) -> bool: + """Is Connected.""" + return self._is_connected + + @property + def mtu_size(self) -> int: + """Get ATT MTU size for active connection.""" + return self._mtu or DEFAULT_MTU + + @verify_connected + @api_error_as_bleak_error + async def pair(self, *args: Any, **kwargs: Any) -> bool: + """Attempt to pair.""" + raise NotImplementedError("Pairing is not available in ESPHome.") + + @verify_connected + @api_error_as_bleak_error + async def unpair(self) -> bool: + """Attempt to unpair.""" + raise NotImplementedError("Pairing is not available in ESPHome.") + + @api_error_as_bleak_error + async def get_services( + self, dangerous_use_bleak_cache: bool = False, **kwargs: Any + ) -> BleakGATTServiceCollection: + """Get all services registered for this GATT server. + + Returns: + 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 + if dangerous_use_bleak_cache and ( + cached_services := domain_data.get_gatt_services_cache(address_as_int) + ): + _LOGGER.debug( + "Cached services hit for %s - %s", + self._ble_device.name, + self._ble_device.address, + ) + self.services = cached_services + return self.services + _LOGGER.debug( + "Cached services miss for %s - %s", + self._ble_device.name, + self._ble_device.address, + ) + esphome_services = await self._client.bluetooth_gatt_get_services( + address_as_int + ) + max_write_without_response = self.mtu_size - GATT_HEADER_SIZE + services = BleakGATTServiceCollection() # type: ignore[no-untyped-call] + for service in esphome_services.services: + services.add_service(BleakGATTServiceESPHome(service)) + for characteristic in service.characteristics: + services.add_characteristic( + BleakGATTCharacteristicESPHome( + characteristic, + max_write_without_response, + service.uuid, + service.handle, + ) + ) + for descriptor in characteristic.descriptors: + services.add_descriptor( + BleakGATTDescriptorESPHome( + descriptor, + characteristic.uuid, + characteristic.handle, + ) + ) + self.services = services + _LOGGER.debug( + "Cached services saved for %s - %s", + self._ble_device.name, + self._ble_device.address, + ) + domain_data.set_gatt_services_cache(address_as_int, services) + return services + + def _resolve_characteristic( + self, char_specifier: BleakGATTCharacteristic | int | str | uuid.UUID + ) -> BleakGATTCharacteristic: + """Resolve a characteristic specifier to a BleakGATTCharacteristic object.""" + if not isinstance(char_specifier, BleakGATTCharacteristic): + characteristic = self.services.get_characteristic(char_specifier) + else: + characteristic = char_specifier + if not characteristic: + raise BleakError(f"Characteristic {char_specifier} was not found!") + return characteristic + + @verify_connected + @api_error_as_bleak_error + async def read_gatt_char( + self, + char_specifier: BleakGATTCharacteristic | int | str | uuid.UUID, + **kwargs: Any, + ) -> bytearray: + """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. + Returns: + (bytearray) The read data. + """ + characteristic = self._resolve_characteristic(char_specifier) + return await self._client.bluetooth_gatt_read( + self._address_as_int, characteristic.handle, GATT_READ_TIMEOUT + ) + + @verify_connected + @api_error_as_bleak_error + async def read_gatt_descriptor(self, handle: int, **kwargs: Any) -> bytearray: + """Perform read operation on the specified GATT descriptor. + + Args: + handle (int): The handle of the descriptor to read from. + Returns: + (bytearray) The read data. + """ + return await self._client.bluetooth_gatt_read_descriptor( + self._address_as_int, handle, GATT_READ_TIMEOUT + ) + + @verify_connected + @api_error_as_bleak_error + async def write_gatt_char( + self, + char_specifier: BleakGATTCharacteristic | int | str | uuid.UUID, + data: bytes | bytearray | memoryview, + response: bool = False, + ) -> None: + """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. + data (bytes or bytearray): The data to send. + 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( + self._address_as_int, characteristic.handle, bytes(data), response + ) + + @verify_connected + @api_error_as_bleak_error + async def write_gatt_descriptor( + self, handle: int, data: bytes | bytearray | memoryview + ) -> None: + """Perform a write operation on the specified GATT descriptor. + + Args: + handle (int): The handle of the descriptor to read from. + data (bytes or bytearray): The data to send. + """ + await self._client.bluetooth_gatt_write_descriptor( + self._address_as_int, handle, bytes(data) + ) + + @verify_connected + @api_error_as_bleak_error + async def start_notify( + self, + characteristic: BleakGATTCharacteristic, + callback: NotifyCallback, + **kwargs: Any, + ) -> 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. + .. 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. + callback (function): The function to be called on notification. + """ + cancel_coro = await self._client.bluetooth_gatt_start_notify( + self._address_as_int, + characteristic.handle, + lambda handle, data: callback(data), + ) + self._notify_cancels[characteristic.handle] = cancel_coro + + @api_error_as_bleak_error + async def stop_notify( + self, + char_specifier: BleakGATTCharacteristic | int | str | uuid.UUID, + ) -> None: + """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. + """ + characteristic = self._resolve_characteristic(char_specifier) + coro = self._notify_cancels.pop(characteristic.handle) + await coro() diff --git a/homeassistant/components/esphome/bluetooth/descriptor.py b/homeassistant/components/esphome/bluetooth/descriptor.py new file mode 100644 index 00000000000..0ba11639740 --- /dev/null +++ b/homeassistant/components/esphome/bluetooth/descriptor.py @@ -0,0 +1,42 @@ +"""BleakGATTDescriptorESPHome.""" +from __future__ import annotations + +from aioesphomeapi.model import BluetoothGATTDescriptor +from bleak.backends.descriptor import BleakGATTDescriptor + + +class BleakGATTDescriptorESPHome(BleakGATTDescriptor): + """GATT Descriptor implementation for ESPHome backend.""" + + obj: BluetoothGATTDescriptor + + def __init__( + self, + obj: BluetoothGATTDescriptor, + characteristic_uuid: str, + characteristic_handle: int, + ) -> None: + """Init a BleakGATTDescriptorESPHome.""" + super().__init__(obj) + self.__characteristic_uuid: str = characteristic_uuid + self.__characteristic_handle: int = characteristic_handle + + @property + def characteristic_handle(self) -> int: + """Handle for the characteristic that this descriptor belongs to.""" + return self.__characteristic_handle + + @property + def characteristic_uuid(self) -> str: + """UUID for the characteristic that this descriptor belongs to.""" + return self.__characteristic_uuid + + @property + def uuid(self) -> str: + """UUID for this descriptor.""" + return self.obj.uuid + + @property + def handle(self) -> int: + """Integer handle for this descriptor.""" + return self.obj.handle diff --git a/homeassistant/components/esphome/bluetooth.py b/homeassistant/components/esphome/bluetooth/scanner.py similarity index 66% rename from homeassistant/components/esphome/bluetooth.py rename to homeassistant/components/esphome/bluetooth/scanner.py index 94351293c7b..36138192f8f 100644 --- a/homeassistant/components/esphome/bluetooth.py +++ b/homeassistant/components/esphome/bluetooth/scanner.py @@ -1,4 +1,5 @@ """Bluetooth scanner for esphome.""" +from __future__ import annotations from collections.abc import Callable import datetime @@ -6,38 +7,27 @@ from datetime import timedelta import re import time -from aioesphomeapi import APIClient, BluetoothLEAdvertisement +from aioesphomeapi import BluetoothLEAdvertisement from bleak.backends.device import BLEDevice from bleak.backends.scanner import AdvertisementData -from homeassistant.components.bluetooth import ( - BaseHaScanner, - async_get_advertisement_callback, - async_register_scanner, -) +from homeassistant.components.bluetooth import BaseHaScanner, HaBluetoothConnector from homeassistant.components.bluetooth.models import BluetoothServiceInfoBleak -from homeassistant.config_entries import ConfigEntry from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.helpers.event import async_track_time_interval -ADV_STALE_TIME = 180 # seconds +# We have to set this quite high as we don't know +# when devices fall out of the esphome device's stack +# like we do with BlueZ so its safer to assume its available +# since if it does go out of range and it is in range +# of another device the timeout is much shorter and it will +# switch over to using that adapter anyways. +ADV_STALE_TIME = 60 * 15 # seconds TWO_CHAR = re.compile("..") -async def async_connect_scanner( - hass: HomeAssistant, entry: ConfigEntry, cli: APIClient -) -> None: - """Connect scanner.""" - assert entry.unique_id is not None - new_info_callback = async_get_advertisement_callback(hass) - scanner = ESPHomeScannner(hass, entry.unique_id, new_info_callback) - entry.async_on_unload(async_register_scanner(hass, scanner, False)) - entry.async_on_unload(scanner.async_setup()) - await cli.subscribe_bluetooth_le_advertisements(scanner.async_on_advertisement) - - -class ESPHomeScannner(BaseHaScanner): +class ESPHomeScanner(BaseHaScanner): """Scanner for esphome.""" def __init__( @@ -45,6 +35,8 @@ class ESPHomeScannner(BaseHaScanner): hass: HomeAssistant, scanner_id: str, new_info_callback: Callable[[BluetoothServiceInfoBleak], None], + connector: HaBluetoothConnector, + connectable: bool, ) -> None: """Initialize the scanner.""" self._hass = hass @@ -52,6 +44,11 @@ class ESPHomeScannner(BaseHaScanner): self._discovered_devices: dict[str, BLEDevice] = {} self._discovered_device_timestamps: dict[str, float] = {} self._source = scanner_id + self._connector = connector + self._connectable = connectable + self._details: dict[str, str | HaBluetoothConnector] = {"source": scanner_id} + if connectable: + self._details["connector"] = connector @callback def async_setup(self) -> CALLBACK_TYPE: @@ -77,21 +74,33 @@ class ESPHomeScannner(BaseHaScanner): """Return a list of discovered devices.""" return list(self._discovered_devices.values()) + async def async_get_device_by_address(self, address: str) -> BLEDevice | None: + """Get a device by address.""" + return self._discovered_devices.get(address) + @callback def async_on_advertisement(self, adv: BluetoothLEAdvertisement) -> None: """Call the registered callback.""" now = time.monotonic() address = ":".join(TWO_CHAR.findall("%012X" % adv.address)) # must be upper + name = adv.name + if prev_discovery := self._discovered_devices.get(address): + # If the last discovery had the full local name + # and this one doesn't, keep the old one as we + # always want the full local name over the short one + if len(prev_discovery.name) > len(adv.name): + name = prev_discovery.name + advertisement_data = AdvertisementData( # type: ignore[no-untyped-call] - local_name=None if adv.name == "" else adv.name, + local_name=None if name == "" else name, manufacturer_data=adv.manufacturer_data, service_data=adv.service_data, service_uuids=adv.service_uuids, ) device = BLEDevice( # type: ignore[no-untyped-call] address=address, - name=adv.name, - details={}, + name=name, + details=self._details, rssi=adv.rssi, ) self._discovered_devices[address] = device @@ -107,7 +116,7 @@ class ESPHomeScannner(BaseHaScanner): source=self._source, device=device, advertisement=advertisement_data, - connectable=False, + connectable=self._connectable, time=now, ) ) diff --git a/homeassistant/components/esphome/bluetooth/service.py b/homeassistant/components/esphome/bluetooth/service.py new file mode 100644 index 00000000000..5df7d2bf603 --- /dev/null +++ b/homeassistant/components/esphome/bluetooth/service.py @@ -0,0 +1,40 @@ +"""BleakGATTServiceESPHome.""" +from __future__ import annotations + +from aioesphomeapi.model import BluetoothGATTService +from bleak.backends.characteristic import BleakGATTCharacteristic +from bleak.backends.service import BleakGATTService + + +class BleakGATTServiceESPHome(BleakGATTService): + """GATT Characteristic implementation for the ESPHome backend.""" + + obj: BluetoothGATTService + + def __init__(self, obj: BluetoothGATTService) -> None: + """Init a BleakGATTServiceESPHome.""" + super().__init__(obj) # type: ignore[no-untyped-call] + self.__characteristics: list[BleakGATTCharacteristic] = [] + self.__handle: int = self.obj.handle + + @property + def handle(self) -> int: + """Integer handle of this service.""" + return self.__handle + + @property + def uuid(self) -> str: + """UUID for this service.""" + return self.obj.uuid + + @property + def characteristics(self) -> list[BleakGATTCharacteristic]: + """List of characteristics for this service.""" + return self.__characteristics + + def add_characteristic(self, characteristic: BleakGATTCharacteristic) -> None: + """Add a :py:class:`~BleakGATTCharacteristicESPHome` to the service. + + Should not be used by end user, but rather by `bleak` itself. + """ + self.__characteristics.append(characteristic) diff --git a/homeassistant/components/esphome/climate.py b/homeassistant/components/esphome/climate.py index aba51a47a9d..4f38d1caa24 100644 --- a/homeassistant/components/esphome/climate.py +++ b/homeassistant/components/esphome/climate.py @@ -13,8 +13,7 @@ from aioesphomeapi import ( ClimateSwingMode, ) -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( ATTR_HVAC_MODE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, @@ -39,6 +38,7 @@ from homeassistant.components.climate.const import ( SWING_HORIZONTAL, SWING_OFF, SWING_VERTICAL, + ClimateEntity, ClimateEntityFeature, HVACAction, HVACMode, @@ -212,13 +212,13 @@ class EsphomeClimateEntity(EsphomeEntity[ClimateInfo, ClimateState], ClimateEnti features |= ClimateEntityFeature.SWING_MODE return features - @property # type: ignore[misc] + @property @esphome_state_property def hvac_mode(self) -> str | None: """Return current operation ie. heat, cool, idle.""" return _CLIMATE_MODES.from_esphome(self._state.mode) - @property # type: ignore[misc] + @property @esphome_state_property def hvac_action(self) -> str | None: """Return current action.""" @@ -227,7 +227,7 @@ class EsphomeClimateEntity(EsphomeEntity[ClimateInfo, ClimateState], ClimateEnti return None return _CLIMATE_ACTIONS.from_esphome(self._state.action) - @property # type: ignore[misc] + @property @esphome_state_property def fan_mode(self) -> str | None: """Return current fan setting.""" @@ -235,7 +235,7 @@ class EsphomeClimateEntity(EsphomeEntity[ClimateInfo, ClimateState], ClimateEnti self._state.fan_mode ) - @property # type: ignore[misc] + @property @esphome_state_property def preset_mode(self) -> str | None: """Return current preset mode.""" @@ -243,31 +243,31 @@ class EsphomeClimateEntity(EsphomeEntity[ClimateInfo, ClimateState], ClimateEnti self._state.preset_compat(self._api_version) ) - @property # type: ignore[misc] + @property @esphome_state_property def swing_mode(self) -> str | None: """Return current swing mode.""" return _SWING_MODES.from_esphome(self._state.swing_mode) - @property # type: ignore[misc] + @property @esphome_state_property def current_temperature(self) -> float | None: """Return the current temperature.""" return self._state.current_temperature - @property # type: ignore[misc] + @property @esphome_state_property def target_temperature(self) -> float | None: """Return the temperature we try to reach.""" return self._state.target_temperature - @property # type: ignore[misc] + @property @esphome_state_property def target_temperature_low(self) -> float | None: """Return the lowbound target temperature we try to reach.""" return self._state.target_temperature_low - @property # type: ignore[misc] + @property @esphome_state_property def target_temperature_high(self) -> float | None: """Return the highbound target temperature we try to reach.""" diff --git a/homeassistant/components/esphome/cover.py b/homeassistant/components/esphome/cover.py index bf179ff25a9..10662977307 100644 --- a/homeassistant/components/esphome/cover.py +++ b/homeassistant/components/esphome/cover.py @@ -65,26 +65,26 @@ class EsphomeCover(EsphomeEntity[CoverInfo, CoverState], CoverEntity): """Return true if we do optimistic updates.""" return self._static_info.assumed_state - @property # type: ignore[misc] + @property @esphome_state_property def is_closed(self) -> bool | None: """Return if the cover is closed or not.""" # Check closed state with api version due to a protocol change return self._state.is_closed(self._api_version) - @property # type: ignore[misc] + @property @esphome_state_property def is_opening(self) -> bool: """Return if the cover is opening or not.""" return self._state.current_operation == CoverOperation.IS_OPENING - @property # type: ignore[misc] + @property @esphome_state_property def is_closing(self) -> bool: """Return if the cover is closing or not.""" return self._state.current_operation == CoverOperation.IS_CLOSING - @property # type: ignore[misc] + @property @esphome_state_property def current_cover_position(self) -> int | None: """Return current position of cover. 0 is closed, 100 is open.""" @@ -92,7 +92,7 @@ class EsphomeCover(EsphomeEntity[CoverInfo, CoverState], CoverEntity): return None return round(self._state.position * 100.0) - @property # type: ignore[misc] + @property @esphome_state_property def current_cover_tilt_position(self) -> int | None: """Return current position of cover tilt. 0 is closed, 100 is open.""" diff --git a/homeassistant/components/esphome/domain_data.py b/homeassistant/components/esphome/domain_data.py new file mode 100644 index 00000000000..acaa76185e7 --- /dev/null +++ b/homeassistant/components/esphome/domain_data.py @@ -0,0 +1,93 @@ +"""Support for esphome domain data.""" +from __future__ import annotations + +from collections.abc import MutableMapping +from dataclasses import dataclass, field +from typing import TypeVar, cast + +from bleak.backends.service import BleakGATTServiceCollection +from lru import LRU # pylint: disable=no-name-in-module + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.json import JSONEncoder +from homeassistant.helpers.storage import Store + +from .entry_data import RuntimeEntryData + +STORAGE_VERSION = 1 +DOMAIN = "esphome" +MAX_CACHED_SERVICES = 128 + +_DomainDataSelfT = TypeVar("_DomainDataSelfT", bound="DomainData") + + +@dataclass +class DomainData: + """Define a class that stores global esphome data in hass.data[DOMAIN].""" + + _entry_datas: dict[str, RuntimeEntryData] = field(default_factory=dict) + _stores: dict[str, Store] = field(default_factory=dict) + _entry_by_unique_id: dict[str, ConfigEntry] = field(default_factory=dict) + _gatt_services_cache: MutableMapping[int, BleakGATTServiceCollection] = field( + default_factory=lambda: LRU(MAX_CACHED_SERVICES) # type: ignore[no-any-return] + ) + + def get_gatt_services_cache( + self, address: int + ) -> BleakGATTServiceCollection | None: + """Get the BleakGATTServiceCollection for the given address.""" + return self._gatt_services_cache.get(address) + + def set_gatt_services_cache( + self, address: int, services: BleakGATTServiceCollection + ) -> None: + """Set the BleakGATTServiceCollection for the given address.""" + self._gatt_services_cache[address] = services + + def get_by_unique_id(self, unique_id: str) -> ConfigEntry: + """Get the config entry by its unique ID.""" + return self._entry_by_unique_id[unique_id] + + def get_entry_data(self, entry: ConfigEntry) -> RuntimeEntryData: + """Return the runtime entry data associated with this config entry. + + Raises KeyError if the entry isn't loaded yet. + """ + return self._entry_datas[entry.entry_id] + + def set_entry_data(self, entry: ConfigEntry, entry_data: RuntimeEntryData) -> None: + """Set the runtime entry data associated with this config entry.""" + if entry.entry_id in self._entry_datas: + raise ValueError("Entry data for this entry is already set") + self._entry_datas[entry.entry_id] = entry_data + if entry.unique_id: + self._entry_by_unique_id[entry.unique_id] = entry + + def pop_entry_data(self, entry: ConfigEntry) -> RuntimeEntryData: + """Pop the runtime entry data instance associated with this config entry.""" + if entry.unique_id: + del self._entry_by_unique_id[entry.unique_id] + return self._entry_datas.pop(entry.entry_id) + + def is_entry_loaded(self, entry: ConfigEntry) -> bool: + """Check whether the given entry is loaded.""" + return entry.entry_id in self._entry_datas + + def get_or_create_store(self, hass: HomeAssistant, entry: ConfigEntry) -> Store: + """Get or create a Store instance for the given config entry.""" + return self._stores.setdefault( + entry.entry_id, + Store( + hass, STORAGE_VERSION, f"esphome.{entry.entry_id}", encoder=JSONEncoder + ), + ) + + @classmethod + def get(cls: type[_DomainDataSelfT], hass: HomeAssistant) -> _DomainDataSelfT: + """Get the global DomainData instance stored in hass.data.""" + # Don't use setdefault - this is a hot code path + if DOMAIN in hass.data: + return cast(_DomainDataSelfT, hass.data[DOMAIN]) + ret = hass.data[DOMAIN] = cls() + return ret diff --git a/homeassistant/components/esphome/entry_data.py b/homeassistant/components/esphome/entry_data.py index 80fd855379e..ac2a148d899 100644 --- a/homeassistant/components/esphome/entry_data.py +++ b/homeassistant/components/esphome/entry_data.py @@ -87,6 +87,31 @@ class RuntimeEntryData: loaded_platforms: set[str] = 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 + ble_connections_limit: int = 0 + _ble_connection_free_futures: list[asyncio.Future[int]] = field( + default_factory=list + ) + + @callback + def async_update_ble_connection_limits(self, free: int, limit: int) -> None: + """Update the BLE connection limits.""" + name = self.device_info.name if self.device_info else self.entry_id + _LOGGER.debug("%s: BLE connection limits: %s/%s", name, free, limit) + self.ble_connections_free = free + self.ble_connections_limit = limit + if free: + for fut in self._ble_connection_free_futures: + fut.set_result(free) + self._ble_connection_free_futures.clear() + + async def wait_for_ble_connections_free(self) -> int: + """Wait until there are free BLE connections.""" + if self.ble_connections_free > 0: + return self.ble_connections_free + fut: asyncio.Future[int] = asyncio.Future() + self._ble_connection_free_futures.append(fut) + return await fut @callback def async_remove_entity( diff --git a/homeassistant/components/esphome/fan.py b/homeassistant/components/esphome/fan.py index 207b9d3f9f8..772a1b8befa 100644 --- a/homeassistant/components/esphome/fan.py +++ b/homeassistant/components/esphome/fan.py @@ -112,13 +112,13 @@ class EsphomeFan(EsphomeEntity[FanInfo, FanState], FanEntity): key=self._static_info.key, direction=_FAN_DIRECTIONS.from_hass(direction) ) - @property # type: ignore[misc] + @property @esphome_state_property def is_on(self) -> bool | None: """Return true if the entity is on.""" return self._state.state - @property # type: ignore[misc] + @property @esphome_state_property def percentage(self) -> int | None: """Return the current speed percentage.""" @@ -141,7 +141,7 @@ class EsphomeFan(EsphomeEntity[FanInfo, FanState], FanEntity): return len(ORDERED_NAMED_FAN_SPEEDS) return self._static_info.supported_speed_levels - @property # type: ignore[misc] + @property @esphome_state_property def oscillating(self) -> bool | None: """Return the oscillation state.""" @@ -149,7 +149,7 @@ class EsphomeFan(EsphomeEntity[FanInfo, FanState], FanEntity): return None return self._state.oscillating - @property # type: ignore[misc] + @property @esphome_state_property def current_direction(self) -> str | None: """Return the current fan direction.""" diff --git a/homeassistant/components/esphome/light.py b/homeassistant/components/esphome/light.py index 2f536e82b47..624dfc8950f 100644 --- a/homeassistant/components/esphome/light.py +++ b/homeassistant/components/esphome/light.py @@ -130,7 +130,7 @@ class EsphomeLight(EsphomeEntity[LightInfo, LightState], LightEntity): """Return whether the client supports the new color mode system natively.""" return self._api_version >= APIVersion(1, 6) - @property # type: ignore[misc] + @property @esphome_state_property def is_on(self) -> bool | None: """Return true if the light is on.""" @@ -260,13 +260,13 @@ class EsphomeLight(EsphomeEntity[LightInfo, LightState], LightEntity): data["transition_length"] = kwargs[ATTR_TRANSITION] await self._client.light_command(**data) - @property # type: ignore[misc] + @property @esphome_state_property def brightness(self) -> int | None: """Return the brightness of this light between 0..255.""" return round(self._state.brightness * 255) - @property # type: ignore[misc] + @property @esphome_state_property def color_mode(self) -> str | None: """Return the color mode of the light.""" @@ -277,7 +277,7 @@ class EsphomeLight(EsphomeEntity[LightInfo, LightState], LightEntity): return _color_mode_to_ha(self._state.color_mode) - @property # type: ignore[misc] + @property @esphome_state_property def rgb_color(self) -> tuple[int, int, int] | None: """Return the rgb color value [int, int, int].""" @@ -294,7 +294,7 @@ class EsphomeLight(EsphomeEntity[LightInfo, LightState], LightEntity): round(self._state.blue * self._state.color_brightness * 255), ) - @property # type: ignore[misc] + @property @esphome_state_property def rgbw_color(self) -> tuple[int, int, int, int] | None: """Return the rgbw color value [int, int, int, int].""" @@ -302,7 +302,7 @@ class EsphomeLight(EsphomeEntity[LightInfo, LightState], LightEntity): rgb = cast("tuple[int, int, int]", self.rgb_color) return (*rgb, white) - @property # type: ignore[misc] + @property @esphome_state_property def rgbww_color(self) -> tuple[int, int, int, int, int] | None: """Return the rgbww color value [int, int, int, int, int].""" @@ -330,13 +330,13 @@ class EsphomeLight(EsphomeEntity[LightInfo, LightState], LightEntity): round(self._state.warm_white * 255), ) - @property # type: ignore[misc] + @property @esphome_state_property def color_temp(self) -> float | None: # type: ignore[override] """Return the CT color value in mireds.""" return self._state.color_temperature - @property # type: ignore[misc] + @property @esphome_state_property def effect(self) -> str | None: """Return the current effect.""" diff --git a/homeassistant/components/esphome/lock.py b/homeassistant/components/esphome/lock.py index 62c7c6de0dd..bcfa0131518 100644 --- a/homeassistant/components/esphome/lock.py +++ b/homeassistant/components/esphome/lock.py @@ -49,25 +49,25 @@ class EsphomeLock(EsphomeEntity[LockInfo, LockEntityState], LockEntity): return self._static_info.code_format return None - @property # type: ignore[misc] + @property @esphome_state_property def is_locked(self) -> bool | None: """Return true if the lock is locked.""" return self._state.state == LockState.LOCKED - @property # type: ignore[misc] + @property @esphome_state_property def is_locking(self) -> bool | None: """Return true if the lock is locking.""" return self._state.state == LockState.LOCKING - @property # type: ignore[misc] + @property @esphome_state_property def is_unlocking(self) -> bool | None: """Return true if the lock is unlocking.""" return self._state.state == LockState.UNLOCKING - @property # type: ignore[misc] + @property @esphome_state_property def is_jammed(self) -> bool | None: """Return true if the lock is jammed (incomplete locking).""" diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index 4739c2904ac..066050d796d 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==10.13.0"], + "requirements": ["aioesphomeapi==11.1.0"], "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 17635157754..d7a70737690 100644 --- a/homeassistant/components/esphome/media_player.py +++ b/homeassistant/components/esphome/media_player.py @@ -7,21 +7,19 @@ from aioesphomeapi import ( MediaPlayerCommand, MediaPlayerEntityState, MediaPlayerInfo, - MediaPlayerState, + MediaPlayerState as EspMediaPlayerState, ) from homeassistant.components import media_source from homeassistant.components.media_player import ( + BrowseMedia, MediaPlayerDeviceClass, MediaPlayerEntity, -) -from homeassistant.components.media_player.browse_media import ( - BrowseMedia, + MediaPlayerEntityFeature, + MediaPlayerState, async_process_play_media_url, ) -from homeassistant.components.media_player.const import MediaPlayerEntityFeature from homeassistant.config_entries import ConfigEntry -from homeassistant.const import STATE_IDLE, STATE_PAUSED, STATE_PLAYING from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -50,11 +48,11 @@ async def async_setup_entry( ) -_STATES: EsphomeEnumMapper[MediaPlayerState, str] = EsphomeEnumMapper( +_STATES: EsphomeEnumMapper[EspMediaPlayerState, MediaPlayerState] = EsphomeEnumMapper( { - MediaPlayerState.IDLE: STATE_IDLE, - MediaPlayerState.PLAYING: STATE_PLAYING, - MediaPlayerState.PAUSED: STATE_PAUSED, + EspMediaPlayerState.IDLE: MediaPlayerState.IDLE, + EspMediaPlayerState.PLAYING: MediaPlayerState.PLAYING, + EspMediaPlayerState.PAUSED: MediaPlayerState.PAUSED, } ) @@ -66,19 +64,19 @@ class EsphomeMediaPlayer( _attr_device_class = MediaPlayerDeviceClass.SPEAKER - @property # type: ignore[misc] + @property @esphome_state_property - def state(self) -> str | None: + def state(self) -> MediaPlayerState | None: """Return current state.""" return _STATES.from_esphome(self._state.state) - @property # type: ignore[misc] + @property @esphome_state_property def is_volume_muted(self) -> bool: """Return true if volume is muted.""" return self._state.muted - @property # type: ignore[misc] + @property @esphome_state_property def volume_level(self) -> float | None: """Volume level of the media player (0..1).""" diff --git a/homeassistant/components/esphome/number.py b/homeassistant/components/esphome/number.py index ed721f2db5e..a00d4456227 100644 --- a/homeassistant/components/esphome/number.py +++ b/homeassistant/components/esphome/number.py @@ -74,7 +74,7 @@ class EsphomeNumber(EsphomeEntity[NumberInfo, NumberState], NumberEntity): return NUMBER_MODES.from_esphome(self._static_info.mode) return NumberMode.AUTO - @property # type: ignore[misc] + @property @esphome_state_property def native_value(self) -> float | None: """Return the state of the entity.""" diff --git a/homeassistant/components/esphome/select.py b/homeassistant/components/esphome/select.py index 190fca52889..79af0455346 100644 --- a/homeassistant/components/esphome/select.py +++ b/homeassistant/components/esphome/select.py @@ -36,7 +36,7 @@ class EsphomeSelect(EsphomeEntity[SelectInfo, SelectState], SelectEntity): """Return a set of selectable options.""" return self._static_info.options - @property # type: ignore[misc] + @property @esphome_state_property def current_option(self) -> str | None: """Return the state of the entity.""" diff --git a/homeassistant/components/esphome/sensor.py b/homeassistant/components/esphome/sensor.py index f7f137f4592..4b316f6a640 100644 --- a/homeassistant/components/esphome/sensor.py +++ b/homeassistant/components/esphome/sensor.py @@ -76,7 +76,7 @@ class EsphomeSensor(EsphomeEntity[SensorInfo, SensorState], SensorEntity): """Return if this sensor should force a state update.""" return self._static_info.force_update - @property # type: ignore[misc] + @property @esphome_state_property def native_value(self) -> datetime | str | None: """Return the state of the entity.""" @@ -121,7 +121,7 @@ class EsphomeSensor(EsphomeEntity[SensorInfo, SensorState], SensorEntity): class EsphomeTextSensor(EsphomeEntity[TextSensorInfo, TextSensorState], SensorEntity): """A text sensor implementation for ESPHome.""" - @property # type: ignore[misc] + @property @esphome_state_property def native_value(self) -> str | None: """Return the state of the entity.""" diff --git a/homeassistant/components/esphome/switch.py b/homeassistant/components/esphome/switch.py index 2970edf7af0..db5084df378 100644 --- a/homeassistant/components/esphome/switch.py +++ b/homeassistant/components/esphome/switch.py @@ -36,7 +36,7 @@ class EsphomeSwitch(EsphomeEntity[SwitchInfo, SwitchState], SwitchEntity): """Return true if we do optimistic updates.""" return self._static_info.assumed_state - @property # type: ignore[misc] + @property @esphome_state_property def is_on(self) -> bool | None: """Return true if the switch is on.""" diff --git a/homeassistant/components/esphome/translations/es.json b/homeassistant/components/esphome/translations/es.json index 87c8dc6ddad..82066953472 100644 --- a/homeassistant/components/esphome/translations/es.json +++ b/homeassistant/components/esphome/translations/es.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "error": { "connection_error": "No se puede conectar a ESP. Por favor, aseg\u00farate de que tu archivo YAML contiene una l\u00ednea 'api:'.", diff --git a/homeassistant/components/esphome/translations/pt.json b/homeassistant/components/esphome/translations/pt.json index 636749dd130..5b5e85922d9 100644 --- a/homeassistant/components/esphome/translations/pt.json +++ b/homeassistant/components/esphome/translations/pt.json @@ -10,7 +10,7 @@ "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "resolve_error": "N\u00e3o \u00e9 poss\u00edvel resolver o endere\u00e7o do ESP. Se este erro persistir, defina um endere\u00e7o IP est\u00e1tico: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" }, - "flow_title": "", + "flow_title": "{name}", "step": { "authenticate": { "data": { diff --git a/homeassistant/components/evil_genius_labs/translations/cs.json b/homeassistant/components/evil_genius_labs/translations/cs.json index 7a929c1286f..6633920519c 100644 --- a/homeassistant/components/evil_genius_labs/translations/cs.json +++ b/homeassistant/components/evil_genius_labs/translations/cs.json @@ -1,8 +1,16 @@ { "config": { "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", "timeout": "Vypr\u0161el \u010dasov\u00fd limit pro nav\u00e1z\u00e1n\u00ed spojen\u00ed", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "host": "Hostitel" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/evohome/climate.py b/homeassistant/components/evohome/climate.py index e9eab8c2ae3..0ec64c6b2b1 100644 --- a/homeassistant/components/evohome/climate.py +++ b/homeassistant/components/evohome/climate.py @@ -5,12 +5,12 @@ from datetime import datetime as dt import logging from typing import Any -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( PRESET_AWAY, PRESET_ECO, PRESET_HOME, PRESET_NONE, + ClimateEntity, ClimateEntityFeature, HVACMode, ) diff --git a/homeassistant/components/ezviz/__init__.py b/homeassistant/components/ezviz/__init__.py index 51931f4b104..fbd49102f3c 100644 --- a/homeassistant/components/ezviz/__init__.py +++ b/homeassistant/components/ezviz/__init__.py @@ -1,4 +1,4 @@ -"""Support for Ezviz camera.""" +"""Support for EZVIZ camera.""" import logging from pyezviz.client import EzvizClient @@ -39,7 +39,7 @@ PLATFORMS = [ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Set up Ezviz from a config entry.""" + """Set up EZVIZ from a config entry.""" hass.data.setdefault(DOMAIN, {}) if not entry.options: @@ -56,7 +56,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Fetch Entry id of main account and reload it. for item in hass.config_entries.async_entries(): if item.data.get(CONF_TYPE) == ATTR_TYPE_CLOUD: - _LOGGER.info("Reload Ezviz integration with new camera rtsp entry") + _LOGGER.info("Reload EZVIZ integration with new camera rtsp entry") await hass.config_entries.async_reload(item.entry_id) return True @@ -66,7 +66,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: _get_ezviz_client_instance, entry ) except (InvalidURL, HTTPError, PyEzvizError) as error: - _LOGGER.error("Unable to connect to Ezviz service: %s", str(error)) + _LOGGER.error("Unable to connect to EZVIZ service: %s", str(error)) raise ConfigEntryNotReady from error coordinator = EzvizDataUpdateCoordinator( diff --git a/homeassistant/components/ezviz/binary_sensor.py b/homeassistant/components/ezviz/binary_sensor.py index 43ac914a50c..bab6fa5ca97 100644 --- a/homeassistant/components/ezviz/binary_sensor.py +++ b/homeassistant/components/ezviz/binary_sensor.py @@ -1,4 +1,4 @@ -"""Support for Ezviz binary sensors.""" +"""Support for EZVIZ binary sensors.""" from __future__ import annotations from homeassistant.components.binary_sensor import ( @@ -35,7 +35,7 @@ BINARY_SENSOR_TYPES: dict[str, BinarySensorEntityDescription] = { async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: - """Set up Ezviz sensors based on a config entry.""" + """Set up EZVIZ sensors based on a config entry.""" coordinator: EzvizDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][ DATA_COORDINATOR ] @@ -52,7 +52,7 @@ async def async_setup_entry( class EzvizBinarySensor(EzvizEntity, BinarySensorEntity): - """Representation of a Ezviz sensor.""" + """Representation of a EZVIZ sensor.""" def __init__( self, diff --git a/homeassistant/components/ezviz/camera.py b/homeassistant/components/ezviz/camera.py index 307e1fac185..1f9de4f611c 100644 --- a/homeassistant/components/ezviz/camera.py +++ b/homeassistant/components/ezviz/camera.py @@ -57,7 +57,7 @@ async def async_setup_entry( entry: ConfigEntry, async_add_entities: entity_platform.AddEntitiesCallback, ) -> None: - """Set up Ezviz cameras based on a config entry.""" + """Set up EZVIZ cameras based on a config entry.""" coordinator: EzvizDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][ DATA_COORDINATOR @@ -73,7 +73,7 @@ async def async_setup_entry( if item.unique_id == camera and item.source != SOURCE_IGNORE ] - # There seem to be a bug related to localRtspPort in Ezviz API. + # There seem to be a bug related to localRtspPort in EZVIZ API. local_rtsp_port = ( value["local_rtsp_port"] if value["local_rtsp_port"] != 0 @@ -174,7 +174,7 @@ async def async_setup_entry( class EzvizCamera(EzvizEntity, Camera): - """An implementation of a Ezviz security camera.""" + """An implementation of a EZVIZ security camera.""" def __init__( self, @@ -187,7 +187,7 @@ class EzvizCamera(EzvizEntity, Camera): local_rtsp_port: int, ffmpeg_arguments: str | None, ) -> None: - """Initialize a Ezviz security camera.""" + """Initialize a EZVIZ security camera.""" super().__init__(coordinator, serial) Camera.__init__(self) self.stream_options[CONF_USE_WALLCLOCK_AS_TIMESTAMPS] = True diff --git a/homeassistant/components/ezviz/config_flow.py b/homeassistant/components/ezviz/config_flow.py index 6c334291ee5..61b66280ae8 100644 --- a/homeassistant/components/ezviz/config_flow.py +++ b/homeassistant/components/ezviz/config_flow.py @@ -67,7 +67,7 @@ def _test_camera_rtsp_creds(data): class EzvizConfigFlow(ConfigFlow, domain=DOMAIN): - """Handle a config flow for Ezviz.""" + """Handle a config flow for EZVIZ.""" VERSION = 1 @@ -101,7 +101,7 @@ class EzvizConfigFlow(ConfigFlow, domain=DOMAIN): async def _validate_and_create_camera_rtsp(self, data): """Try DESCRIBE on RTSP camera with credentials.""" - # Get Ezviz cloud credentials from config entry + # Get EZVIZ cloud credentials from config entry ezviz_client_creds = { CONF_USERNAME: None, CONF_PASSWORD: None, @@ -311,14 +311,14 @@ class EzvizConfigFlow(ConfigFlow, domain=DOMAIN): class EzvizOptionsFlowHandler(OptionsFlow): - """Handle Ezviz client options.""" + """Handle EZVIZ client options.""" def __init__(self, config_entry: ConfigEntry) -> None: """Initialize options flow.""" self.config_entry = config_entry async def async_step_init(self, user_input=None): - """Manage Ezviz options.""" + """Manage EZVIZ options.""" if user_input is not None: return self.async_create_entry(title="", data=user_input) diff --git a/homeassistant/components/ezviz/const.py b/homeassistant/components/ezviz/const.py index 5340f48d0f6..b9183772b6c 100644 --- a/homeassistant/components/ezviz/const.py +++ b/homeassistant/components/ezviz/const.py @@ -1,7 +1,7 @@ """Constants for the ezviz integration.""" DOMAIN = "ezviz" -MANUFACTURER = "Ezviz" +MANUFACTURER = "EZVIZ" # Configuration ATTR_SERIAL = "serial" diff --git a/homeassistant/components/ezviz/coordinator.py b/homeassistant/components/ezviz/coordinator.py index 8729aa4cf21..cc4537bb9b9 100644 --- a/homeassistant/components/ezviz/coordinator.py +++ b/homeassistant/components/ezviz/coordinator.py @@ -15,12 +15,12 @@ _LOGGER = logging.getLogger(__name__) class EzvizDataUpdateCoordinator(DataUpdateCoordinator): - """Class to manage fetching Ezviz data.""" + """Class to manage fetching EZVIZ data.""" def __init__( self, hass: HomeAssistant, *, api: EzvizClient, api_timeout: int ) -> None: - """Initialize global Ezviz data updater.""" + """Initialize global EZVIZ data updater.""" self.ezviz_client = api self._api_timeout = api_timeout update_interval = timedelta(seconds=30) @@ -28,11 +28,11 @@ class EzvizDataUpdateCoordinator(DataUpdateCoordinator): super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=update_interval) def _update_data(self) -> dict: - """Fetch data from Ezviz via camera load function.""" + """Fetch data from EZVIZ via camera load function.""" return self.ezviz_client.load_cameras() async def _async_update_data(self) -> dict: - """Fetch data from Ezviz.""" + """Fetch data from EZVIZ.""" try: async with timeout(self._api_timeout): return await self.hass.async_add_executor_job(self._update_data) diff --git a/homeassistant/components/ezviz/entity.py b/homeassistant/components/ezviz/entity.py index 2ab42a93286..e4debedc640 100644 --- a/homeassistant/components/ezviz/entity.py +++ b/homeassistant/components/ezviz/entity.py @@ -1,4 +1,4 @@ -"""An abstract class common to all Ezviz entities.""" +"""An abstract class common to all EZVIZ entities.""" from __future__ import annotations from typing import Any @@ -11,7 +11,7 @@ from .coordinator import EzvizDataUpdateCoordinator class EzvizEntity(CoordinatorEntity[EzvizDataUpdateCoordinator], Entity): - """Generic entity encapsulating common features of Ezviz device.""" + """Generic entity encapsulating common features of EZVIZ device.""" def __init__( self, diff --git a/homeassistant/components/ezviz/manifest.json b/homeassistant/components/ezviz/manifest.json index 47e2ec44e8a..985c96de806 100644 --- a/homeassistant/components/ezviz/manifest.json +++ b/homeassistant/components/ezviz/manifest.json @@ -1,6 +1,6 @@ { "domain": "ezviz", - "name": "Ezviz", + "name": "EZVIZ", "documentation": "https://www.home-assistant.io/integrations/ezviz", "dependencies": ["ffmpeg"], "codeowners": ["@RenierM26", "@baqs"], diff --git a/homeassistant/components/ezviz/sensor.py b/homeassistant/components/ezviz/sensor.py index a7334a3d18b..8e617aa3b3e 100644 --- a/homeassistant/components/ezviz/sensor.py +++ b/homeassistant/components/ezviz/sensor.py @@ -1,4 +1,4 @@ -"""Support for Ezviz sensors.""" +"""Support for EZVIZ sensors.""" from __future__ import annotations from homeassistant.components.sensor import ( @@ -44,7 +44,7 @@ SENSOR_TYPES: dict[str, SensorEntityDescription] = { async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: - """Set up Ezviz sensors based on a config entry.""" + """Set up EZVIZ sensors based on a config entry.""" coordinator: EzvizDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][ DATA_COORDINATOR ] @@ -61,7 +61,7 @@ async def async_setup_entry( class EzvizSensor(EzvizEntity, SensorEntity): - """Representation of a Ezviz sensor.""" + """Representation of a EZVIZ sensor.""" coordinator: EzvizDataUpdateCoordinator diff --git a/homeassistant/components/ezviz/strings.json b/homeassistant/components/ezviz/strings.json index a8831d2ae34..91fa32ad9b2 100644 --- a/homeassistant/components/ezviz/strings.json +++ b/homeassistant/components/ezviz/strings.json @@ -3,7 +3,7 @@ "flow_title": "{serial}", "step": { "user": { - "title": "Connect to Ezviz Cloud", + "title": "Connect to EZVIZ Cloud", "data": { "username": "[%key:common::config_flow::data::username%]", "password": "[%key:common::config_flow::data::password%]", @@ -11,7 +11,7 @@ } }, "user_custom_url": { - "title": "Connect to custom Ezviz URL", + "title": "Connect to custom EZVIZ URL", "description": "Manually specify your region URL", "data": { "username": "[%key:common::config_flow::data::username%]", @@ -20,8 +20,8 @@ } }, "confirm": { - "title": "Discovered Ezviz Camera", - "description": "Enter RTSP credentials for Ezviz camera {serial} with IP {ip_address}", + "title": "Discovered EZVIZ Camera", + "description": "Enter RTSP credentials for EZVIZ camera {serial} with IP {ip_address}", "data": { "username": "[%key:common::config_flow::data::username%]", "password": "[%key:common::config_flow::data::password%]" @@ -36,7 +36,7 @@ "abort": { "already_configured_account": "[%key:common::config_flow::abort::already_configured_account%]", "unknown": "[%key:common::config_flow::error::unknown%]", - "ezviz_cloud_account_missing": "Ezviz cloud account missing. Please reconfigure Ezviz cloud account" + "ezviz_cloud_account_missing": "EZVIZ cloud account missing. Please reconfigure EZVIZ cloud account" } }, "options": { diff --git a/homeassistant/components/ezviz/switch.py b/homeassistant/components/ezviz/switch.py index 55a946f858a..58b28477412 100644 --- a/homeassistant/components/ezviz/switch.py +++ b/homeassistant/components/ezviz/switch.py @@ -1,4 +1,4 @@ -"""Support for Ezviz Switch sensors.""" +"""Support for EZVIZ Switch sensors.""" from __future__ import annotations from typing import Any @@ -19,7 +19,7 @@ from .entity import EzvizEntity async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: - """Set up Ezviz switch based on a config entry.""" + """Set up EZVIZ switch based on a config entry.""" coordinator: EzvizDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][ DATA_COORDINATOR ] @@ -37,7 +37,7 @@ async def async_setup_entry( class EzvizSwitch(EzvizEntity, SwitchEntity): - """Representation of a Ezviz sensor.""" + """Representation of a EZVIZ sensor.""" _attr_device_class = SwitchDeviceClass.SWITCH diff --git a/homeassistant/components/ezviz/translations/en.json b/homeassistant/components/ezviz/translations/en.json index 9b5e273b0ad..c9f096e7995 100644 --- a/homeassistant/components/ezviz/translations/en.json +++ b/homeassistant/components/ezviz/translations/en.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured_account": "Account is already configured", - "ezviz_cloud_account_missing": "Ezviz cloud account missing. Please reconfigure Ezviz cloud account", + "ezviz_cloud_account_missing": "EZVIZ cloud account missing. Please reconfigure EZVIZ cloud account", "unknown": "Unexpected error" }, "error": { @@ -17,8 +17,8 @@ "password": "Password", "username": "Username" }, - "description": "Enter RTSP credentials for Ezviz camera {serial} with IP {ip_address}", - "title": "Discovered Ezviz Camera" + "description": "Enter RTSP credentials for EZVIZ camera {serial} with IP {ip_address}", + "title": "Discovered EZVIZ Camera" }, "user": { "data": { @@ -26,7 +26,7 @@ "url": "URL", "username": "Username" }, - "title": "Connect to Ezviz Cloud" + "title": "Connect to EZVIZ Cloud" }, "user_custom_url": { "data": { @@ -35,7 +35,7 @@ "username": "Username" }, "description": "Manually specify your region URL", - "title": "Connect to custom Ezviz URL" + "title": "Connect to custom EZVIZ URL" } } }, diff --git a/homeassistant/components/fan/__init__.py b/homeassistant/components/fan/__init__.py index 8f6585f6535..d44335fad07 100644 --- a/homeassistant/components/fan/__init__.py +++ b/homeassistant/components/fan/__init__.py @@ -74,6 +74,8 @@ ATTR_DIRECTION = "direction" ATTR_PRESET_MODE = "preset_mode" ATTR_PRESET_MODES = "preset_modes" +# mypy: disallow-any-generics + class NotValidPresetModeError(ValueError): """Exception class when the preset_mode in not in the preset_modes list.""" @@ -89,7 +91,7 @@ def is_on(hass: HomeAssistant, entity_id: str) -> bool: async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Expose fan control via statemachine and services.""" - component = hass.data[DOMAIN] = EntityComponent( + component = hass.data[DOMAIN] = EntityComponent[FanEntity]( _LOGGER, DOMAIN, hass, SCAN_INTERVAL ) @@ -163,13 +165,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry.""" - component: EntityComponent = hass.data[DOMAIN] + component: EntityComponent[FanEntity] = hass.data[DOMAIN] return await component.async_setup_entry(entry) async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - component: EntityComponent = hass.data[DOMAIN] + component: EntityComponent[FanEntity] = hass.data[DOMAIN] return await component.async_unload_entry(entry) diff --git a/homeassistant/components/fan/manifest.json b/homeassistant/components/fan/manifest.json index bb968240f0b..f25162d9959 100644 --- a/homeassistant/components/fan/manifest.json +++ b/homeassistant/components/fan/manifest.json @@ -3,5 +3,6 @@ "name": "Fan", "documentation": "https://www.home-assistant.io/integrations/fan", "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "entity" } diff --git a/homeassistant/components/fibaro/__init__.py b/homeassistant/components/fibaro/__init__.py index 7ab83d796f2..9c2d252d77f 100644 --- a/homeassistant/components/fibaro/__init__.py +++ b/homeassistant/components/fibaro/__init__.py @@ -27,7 +27,11 @@ from homeassistant.const import ( Platform, ) from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError +from homeassistant.exceptions import ( + ConfigEntryAuthFailed, + ConfigEntryNotReady, + HomeAssistantError, +) from homeassistant.helpers import device_registry as dr import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import DeviceInfo, Entity @@ -497,8 +501,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: raise ConfigEntryNotReady( f"Could not connect to controller at {entry.data[CONF_URL]}" ) from connect_ex - except FibaroAuthFailed: - return False + except FibaroAuthFailed as auth_ex: + raise ConfigEntryAuthFailed from auth_ex data: dict[str, Any] = {} hass.data.setdefault(DOMAIN, {})[entry.entry_id] = data diff --git a/homeassistant/components/fibaro/climate.py b/homeassistant/components/fibaro/climate.py index 94dd599698d..90a13fe8988 100644 --- a/homeassistant/components/fibaro/climate.py +++ b/homeassistant/components/fibaro/climate.py @@ -4,10 +4,11 @@ from __future__ import annotations import logging from typing import Any -from homeassistant.components.climate import ENTITY_ID_FORMAT, ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( + ENTITY_ID_FORMAT, PRESET_AWAY, PRESET_BOOST, + ClimateEntity, ClimateEntityFeature, HVACMode, ) diff --git a/homeassistant/components/fibaro/config_flow.py b/homeassistant/components/fibaro/config_flow.py index fd53bd5b94f..7a6d7422520 100644 --- a/homeassistant/components/fibaro/config_flow.py +++ b/homeassistant/components/fibaro/config_flow.py @@ -1,6 +1,7 @@ """Config flow for Fibaro integration.""" from __future__ import annotations +from collections.abc import Mapping import logging from typing import Any @@ -58,6 +59,10 @@ class FibaroConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 + def __init__(self) -> None: + """Initialize.""" + self._reauth_entry: config_entries.ConfigEntry | None = None + async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: @@ -83,3 +88,43 @@ class FibaroConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_import(self, import_config: ConfigType | None) -> FlowResult: """Import a config entry.""" return await self.async_step_user(import_config) + + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + """Handle reauthentication.""" + 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: + """Handle a flow initiated by reauthentication.""" + errors = {} + + assert self._reauth_entry + if user_input is not None: + new_data = self._reauth_entry.data | user_input + try: + await _validate_input(self.hass, new_data) + except FibaroConnectFailed: + errors["base"] = "cannot_connect" + except FibaroAuthFailed: + errors["base"] = "invalid_auth" + else: + self.hass.config_entries.async_update_entry( + self._reauth_entry, data=new_data + ) + self.hass.async_create_task( + self.hass.config_entries.async_reload(self._reauth_entry.entry_id) + ) + return self.async_abort(reason="reauth_successful") + + return self.async_show_form( + step_id="reauth_confirm", + data_schema=vol.Schema({vol.Required(CONF_PASSWORD): str}), + errors=errors, + description_placeholders={ + CONF_USERNAME: self._reauth_entry.data[CONF_USERNAME] + }, + ) diff --git a/homeassistant/components/fibaro/strings.json b/homeassistant/components/fibaro/strings.json index 99c25c9f6e0..de875176cdb 100644 --- a/homeassistant/components/fibaro/strings.json +++ b/homeassistant/components/fibaro/strings.json @@ -8,6 +8,13 @@ "password": "[%key:common::config_flow::data::password%]", "import_plugins": "Import entities from fibaro plugins?" } + }, + "reauth_confirm": { + "data": { + "password": "[%key:common::config_flow::data::password%]" + }, + "title": "[%key:common::config_flow::title::reauth%]", + "description": "Please update your password for {username}" } }, "error": { @@ -16,7 +23,8 @@ "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } } } diff --git a/homeassistant/components/fibaro/translations/bg.json b/homeassistant/components/fibaro/translations/bg.json index 9f39b8c9185..3eab800c93f 100644 --- a/homeassistant/components/fibaro/translations/bg.json +++ b/homeassistant/components/fibaro/translations/bg.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", @@ -9,6 +10,13 @@ "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "step": { + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430" + }, + "description": "\u041c\u043e\u043b\u044f, \u0430\u043a\u0442\u0443\u0430\u043b\u0438\u0437\u0438\u0440\u0430\u0439\u0442\u0435 \u043f\u0430\u0440\u043e\u043b\u0430\u0442\u0430 \u0441\u0438 \u0437\u0430 {username}", + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u043d\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430" + }, "user": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u0430", diff --git a/homeassistant/components/fibaro/translations/ca.json b/homeassistant/components/fibaro/translations/ca.json index 8e04b3567f8..01ffd644a3c 100644 --- a/homeassistant/components/fibaro/translations/ca.json +++ b/homeassistant/components/fibaro/translations/ca.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "El dispositiu ja est\u00e0 configurat" + "already_configured": "El dispositiu ja est\u00e0 configurat", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3", @@ -9,6 +10,13 @@ "unknown": "Error inesperat" }, "step": { + "reauth_confirm": { + "data": { + "password": "Contrasenya" + }, + "description": "Si us plau, actualitza la contrasenya de {username}", + "title": "Reautenticaci\u00f3 de la integraci\u00f3" + }, "user": { "data": { "import_plugins": "Vols importar les entitats dels complements Fibaro?", diff --git a/homeassistant/components/fibaro/translations/cs.json b/homeassistant/components/fibaro/translations/cs.json index e1bf8e7f45f..7cc900ee748 100644 --- a/homeassistant/components/fibaro/translations/cs.json +++ b/homeassistant/components/fibaro/translations/cs.json @@ -1,7 +1,28 @@ { "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" + }, "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Heslo" + }, + "description": "Pros\u00edm aktualizujte sv\u00e9 heslo k \u00fa\u010dtu {username}", + "title": "Znovu ov\u011b\u0159it integraci" + }, + "user": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/fibaro/translations/de.json b/homeassistant/components/fibaro/translations/de.json index 831abc85929..e16bd4a56a7 100644 --- a/homeassistant/components/fibaro/translations/de.json +++ b/homeassistant/components/fibaro/translations/de.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Ger\u00e4t ist bereits konfiguriert" + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", @@ -9,6 +10,13 @@ "unknown": "Unerwarteter Fehler" }, "step": { + "reauth_confirm": { + "data": { + "password": "Passwort" + }, + "description": "Bitte \u00e4ndere Dein Passwort f\u00fcr {username}", + "title": "Integration erneut authentifizieren" + }, "user": { "data": { "import_plugins": "Entit\u00e4ten aus Fibaro-Plugins importieren?", diff --git a/homeassistant/components/fibaro/translations/el.json b/homeassistant/components/fibaro/translations/el.json index d2a2659646f..528ae82b54f 100644 --- a/homeassistant/components/fibaro/translations/el.json +++ b/homeassistant/components/fibaro/translations/el.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af" + "already_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", @@ -9,6 +10,13 @@ "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, "step": { + "reauth_confirm": { + "data": { + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2" + }, + "description": "\u0395\u03bd\u03b7\u03bc\u03b5\u03c1\u03ce\u03c3\u03c4\u03b5 \u03c4\u03bf\u03bd \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03ae\u03c2 \u03c3\u03b1\u03c2 \u03b3\u03b9\u03b1 {username}", + "title": "\u0395\u03c0\u03b1\u03bd\u03b1\u03bb\u03b7\u03c0\u03c4\u03b9\u03ba\u03cc\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2" + }, "user": { "data": { "import_plugins": "\u0395\u03b9\u03c3\u03b1\u03b3\u03c9\u03b3\u03ae \u03bf\u03bd\u03c4\u03bf\u03c4\u03ae\u03c4\u03c9\u03bd \u03b1\u03c0\u03cc \u03c0\u03c1\u03cc\u03c3\u03b8\u03b5\u03c4\u03b1 fibaro;", diff --git a/homeassistant/components/fibaro/translations/en.json b/homeassistant/components/fibaro/translations/en.json index 6bcff530798..e762ef718a0 100644 --- a/homeassistant/components/fibaro/translations/en.json +++ b/homeassistant/components/fibaro/translations/en.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Device is already configured" + "already_configured": "Device is already configured", + "reauth_successful": "Re-authentication was successful" }, "error": { "cannot_connect": "Failed to connect", @@ -9,6 +10,13 @@ "unknown": "Unexpected error" }, "step": { + "reauth_confirm": { + "data": { + "password": "Password" + }, + "description": "Please update your password for {username}", + "title": "Reauthenticate Integration" + }, "user": { "data": { "import_plugins": "Import entities from fibaro plugins?", diff --git a/homeassistant/components/fibaro/translations/es.json b/homeassistant/components/fibaro/translations/es.json index 0bf8f1aaa76..3b1f7d81147 100644 --- a/homeassistant/components/fibaro/translations/es.json +++ b/homeassistant/components/fibaro/translations/es.json @@ -1,7 +1,8 @@ { "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": { "cannot_connect": "No se pudo conectar", @@ -9,6 +10,13 @@ "unknown": "Error inesperado" }, "step": { + "reauth_confirm": { + "data": { + "password": "Contrase\u00f1a" + }, + "description": "Por favor, actualiza tu contrase\u00f1a para {username}", + "title": "Volver a autenticar la integraci\u00f3n" + }, "user": { "data": { "import_plugins": "\u00bfImportar entidades desde los plugins de fibaro?", diff --git a/homeassistant/components/fibaro/translations/et.json b/homeassistant/components/fibaro/translations/et.json index d9f140f8380..fa24f26df33 100644 --- a/homeassistant/components/fibaro/translations/et.json +++ b/homeassistant/components/fibaro/translations/et.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "reauth_successful": "Taastuvastamine \u00f5nnestus" }, "error": { "cannot_connect": "\u00dchendamine nurjus", @@ -9,6 +10,13 @@ "unknown": "Ootamatu t\u00f5rge" }, "step": { + "reauth_confirm": { + "data": { + "password": "Salas\u00f5na" + }, + "description": "Uuenda kasutaja {username} salas\u00f5na", + "title": "Taastuvasta sidumine" + }, "user": { "data": { "import_plugins": "Kas importida olemid fibaro pistikprogrammidest?", diff --git a/homeassistant/components/fibaro/translations/fr.json b/homeassistant/components/fibaro/translations/fr.json index 8dee1529959..9b33a524e73 100644 --- a/homeassistant/components/fibaro/translations/fr.json +++ b/homeassistant/components/fibaro/translations/fr.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" }, "error": { "cannot_connect": "\u00c9chec de connexion", @@ -9,6 +10,13 @@ "unknown": "Erreur inattendue" }, "step": { + "reauth_confirm": { + "data": { + "password": "Mot de passe" + }, + "description": "Veuillez mettre \u00e0 jour votre mot de passe pour {username}", + "title": "R\u00e9-authentifier l'int\u00e9gration" + }, "user": { "data": { "import_plugins": "Importer les entit\u00e9s \u00e0 partir des plugins fibaro\u00a0?", diff --git a/homeassistant/components/fibaro/translations/he.json b/homeassistant/components/fibaro/translations/he.json index c479d8488f2..1173a604472 100644 --- a/homeassistant/components/fibaro/translations/he.json +++ b/homeassistant/components/fibaro/translations/he.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7" }, "error": { "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", @@ -9,6 +10,12 @@ "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" }, "step": { + "reauth_confirm": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4" + }, + "title": "\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05e9\u05dc \u05e9\u05d9\u05dc\u05d5\u05d1" + }, "user": { "data": { "password": "\u05e1\u05d9\u05e1\u05de\u05d4", diff --git a/homeassistant/components/fibaro/translations/hu.json b/homeassistant/components/fibaro/translations/hu.json index d976d4b1a96..a5f340715c5 100644 --- a/homeassistant/components/fibaro/translations/hu.json +++ b/homeassistant/components/fibaro/translations/hu.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt." }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", @@ -9,6 +10,13 @@ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "step": { + "reauth_confirm": { + "data": { + "password": "Jelsz\u00f3" + }, + "description": "K\u00e9rem, friss\u00edtse {username} jelszav\u00e1t", + "title": "Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se" + }, "user": { "data": { "import_plugins": "Import\u00e1ln\u00e1 az entit\u00e1sokat a fibaro be\u00e9p\u00fcl\u0151 modulokb\u00f3l?", diff --git a/homeassistant/components/fibaro/translations/id.json b/homeassistant/components/fibaro/translations/id.json index 715ad91c275..b54dd126aed 100644 --- a/homeassistant/components/fibaro/translations/id.json +++ b/homeassistant/components/fibaro/translations/id.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Perangkat sudah dikonfigurasi" + "already_configured": "Perangkat sudah dikonfigurasi", + "reauth_successful": "Autentikasi ulang berhasil" }, "error": { "cannot_connect": "Gagal terhubung", @@ -9,6 +10,13 @@ "unknown": "Kesalahan yang tidak diharapkan" }, "step": { + "reauth_confirm": { + "data": { + "password": "Kata Sandi" + }, + "description": "Perbarui kata sandi Anda untuk {username}", + "title": "Autentikasi Ulang Integrasi" + }, "user": { "data": { "import_plugins": "Impor entitas dari plugin fibaro?", diff --git a/homeassistant/components/fibaro/translations/it.json b/homeassistant/components/fibaro/translations/it.json index 641ed94e49f..82c549faba1 100644 --- a/homeassistant/components/fibaro/translations/it.json +++ b/homeassistant/components/fibaro/translations/it.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" }, "error": { "cannot_connect": "Impossibile connettersi", @@ -9,6 +10,13 @@ "unknown": "Errore imprevisto" }, "step": { + "reauth_confirm": { + "data": { + "password": "Password" + }, + "description": "Aggiorna la tua password per {username}", + "title": "Autentica nuovamente l'integrazione" + }, "user": { "data": { "import_plugins": "Vuoi importare le entit\u00e0 dai plugin fibaro?", diff --git a/homeassistant/components/fibaro/translations/ja.json b/homeassistant/components/fibaro/translations/ja.json index 8fc6562ff3b..e99a5645343 100644 --- a/homeassistant/components/fibaro/translations/ja.json +++ b/homeassistant/components/fibaro/translations/ja.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", @@ -9,6 +10,13 @@ "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "step": { + "reauth_confirm": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + }, + "description": "{username}\u306e\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u66f4\u65b0\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" + }, "user": { "data": { "import_plugins": "fibaro\u30d7\u30e9\u30b0\u30a4\u30f3\u304b\u3089\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u3092\u30a4\u30f3\u30dd\u30fc\u30c8\u3057\u307e\u3059\u304b\uff1f", diff --git a/homeassistant/components/fibaro/translations/nl.json b/homeassistant/components/fibaro/translations/nl.json index 049168c73d8..74cdb76596c 100644 --- a/homeassistant/components/fibaro/translations/nl.json +++ b/homeassistant/components/fibaro/translations/nl.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Apparaat is al geconfigureerd" + "already_configured": "Apparaat is al geconfigureerd", + "reauth_successful": "Herauthenticatie geslaagd" }, "error": { "cannot_connect": "Kan geen verbinding maken", @@ -9,6 +10,13 @@ "unknown": "Onverwachte fout" }, "step": { + "reauth_confirm": { + "data": { + "password": "Wachtwoord" + }, + "description": "Update je wachtwoord voor {username}", + "title": "Integratie herauthenticeren" + }, "user": { "data": { "import_plugins": "Entiteiten importeren uit fibaro-plug-ins?", diff --git a/homeassistant/components/fibaro/translations/no.json b/homeassistant/components/fibaro/translations/no.json index 8c868bb1ad8..f98835533f5 100644 --- a/homeassistant/components/fibaro/translations/no.json +++ b/homeassistant/components/fibaro/translations/no.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Enheten er allerede konfigurert" + "already_configured": "Enheten er allerede konfigurert", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" }, "error": { "cannot_connect": "Tilkobling mislyktes", @@ -9,6 +10,13 @@ "unknown": "Uventet feil" }, "step": { + "reauth_confirm": { + "data": { + "password": "Passord" + }, + "description": "Oppdater passordet ditt for {username}", + "title": "Godkjenne integrering p\u00e5 nytt" + }, "user": { "data": { "import_plugins": "Importere enheter fra fibaro plugins?", diff --git a/homeassistant/components/fibaro/translations/pl.json b/homeassistant/components/fibaro/translations/pl.json index ce4b2652d80..da7240c3e85 100644 --- a/homeassistant/components/fibaro/translations/pl.json +++ b/homeassistant/components/fibaro/translations/pl.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", @@ -9,6 +10,13 @@ "unknown": "Nieoczekiwany b\u0142\u0105d" }, "step": { + "reauth_confirm": { + "data": { + "password": "Has\u0142o" + }, + "description": "Zaktualizuj has\u0142o dla u\u017cytkownika {username}", + "title": "Ponownie uwierzytelnij integracj\u0119" + }, "user": { "data": { "import_plugins": "Zaimportowa\u0107 encje z wtyczek fibaro?", diff --git a/homeassistant/components/fibaro/translations/pt-BR.json b/homeassistant/components/fibaro/translations/pt-BR.json index 3e0f3139fa7..663100b571d 100644 --- a/homeassistant/components/fibaro/translations/pt-BR.json +++ b/homeassistant/components/fibaro/translations/pt-BR.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" }, "error": { "cannot_connect": "Falha ao conectar", @@ -9,6 +10,13 @@ "unknown": "Erro inesperado" }, "step": { + "reauth_confirm": { + "data": { + "password": "Senha" + }, + "description": "Atualize sua senha para {username}", + "title": "Reautenticar Integra\u00e7\u00e3o" + }, "user": { "data": { "import_plugins": "Importar entidades de plugins fibaro?", diff --git a/homeassistant/components/fibaro/translations/pt.json b/homeassistant/components/fibaro/translations/pt.json index db0e0c2a137..f98b7b7b7b3 100644 --- a/homeassistant/components/fibaro/translations/pt.json +++ b/homeassistant/components/fibaro/translations/pt.json @@ -5,6 +5,11 @@ }, "error": { "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "step": { + "reauth_confirm": { + "description": "Atualize sua senha para {username}" + } } } } \ No newline at end of file diff --git a/homeassistant/components/fibaro/translations/ru.json b/homeassistant/components/fibaro/translations/ru.json index 56e75b5fa34..ce065a9d1b5 100644 --- a/homeassistant/components/fibaro/translations/ru.json +++ b/homeassistant/components/fibaro/translations/ru.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant." + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", @@ -9,6 +10,13 @@ "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "step": { + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + }, + "description": "\u041e\u0431\u043d\u043e\u0432\u0438\u0442\u0435 \u043f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f \u0443\u0447\u0451\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 {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": { "import_plugins": "\u0418\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043e\u0431\u044a\u0435\u043a\u0442\u044b \u0438\u0437 \u043f\u043b\u0430\u0433\u0438\u043d\u043e\u0432 fibaro", diff --git a/homeassistant/components/fibaro/translations/sv.json b/homeassistant/components/fibaro/translations/sv.json index e35b7b41d46..0bc73f04dac 100644 --- a/homeassistant/components/fibaro/translations/sv.json +++ b/homeassistant/components/fibaro/translations/sv.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Enheten \u00e4r redan konfigurerad" + "already_configured": "Enheten \u00e4r redan konfigurerad", + "reauth_successful": "\u00c5terautentisering lyckades" }, "error": { "cannot_connect": "Det gick inte att ansluta.", @@ -9,6 +10,13 @@ "unknown": "Ov\u00e4ntat fel" }, "step": { + "reauth_confirm": { + "data": { + "password": "L\u00f6senord" + }, + "description": "Uppdatera ditt l\u00f6senord f\u00f6r {username}", + "title": "\u00c5terautenticera integration" + }, "user": { "data": { "import_plugins": "Importera enheter fr\u00e5n fibaro plugin?", diff --git a/homeassistant/components/fibaro/translations/tr.json b/homeassistant/components/fibaro/translations/tr.json index c873e0dafd3..c4ca357b4bb 100644 --- a/homeassistant/components/fibaro/translations/tr.json +++ b/homeassistant/components/fibaro/translations/tr.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" }, "error": { "cannot_connect": "Ba\u011flanma hatas\u0131", @@ -9,6 +10,13 @@ "unknown": "Beklenmeyen hata" }, "step": { + "reauth_confirm": { + "data": { + "password": "Parola" + }, + "description": "L\u00fctfen {username} i\u00e7in \u015fifrenizi g\u00fcncelleyin", + "title": "Entegrasyonu Yeniden Do\u011frula" + }, "user": { "data": { "import_plugins": "Varl\u0131klar\u0131 fibaro eklentilerinden i\u00e7e aktar\u0131ls\u0131n m\u0131?", diff --git a/homeassistant/components/fibaro/translations/zh-Hant.json b/homeassistant/components/fibaro/translations/zh-Hant.json index e494fb1012f..36bfb518b35 100644 --- a/homeassistant/components/fibaro/translations/zh-Hant.json +++ b/homeassistant/components/fibaro/translations/zh-Hant.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", @@ -9,6 +10,13 @@ "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "step": { + "reauth_confirm": { + "data": { + "password": "\u5bc6\u78bc" + }, + "description": "\u8acb\u66f4\u65b0 {username} \u5bc6\u78bc", + "title": "\u91cd\u65b0\u8a8d\u8b49\u6574\u5408" + }, "user": { "data": { "import_plugins": "\u5f9e fibaro \u5916\u639b\u532f\u5165\u5be6\u9ad4\uff1f", diff --git a/homeassistant/components/file/notify.py b/homeassistant/components/file/notify.py index 4e9a2b39d1b..eb9a512b230 100644 --- a/homeassistant/components/file/notify.py +++ b/homeassistant/components/file/notify.py @@ -15,7 +15,7 @@ from homeassistant.components.notify import ( from homeassistant.const import CONF_FILENAME from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType import homeassistant.util.dt as dt_util CONF_TIMESTAMP = "timestamp" @@ -29,7 +29,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def get_service( - hass: HomeAssistant, config: ConfigType, discovery_info=None + hass: HomeAssistant, + config: ConfigType, + discovery_info: DiscoveryInfoType | None = None, ) -> FileNotificationService: """Get the file notification service.""" filename: str = config[CONF_FILENAME] diff --git a/homeassistant/components/file_upload/manifest.json b/homeassistant/components/file_upload/manifest.json index 6e190ba3712..d2b4f88a279 100644 --- a/homeassistant/components/file_upload/manifest.json +++ b/homeassistant/components/file_upload/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/file_upload", "dependencies": ["http"], "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "system" } diff --git a/homeassistant/components/fireservicerota/translations/es.json b/homeassistant/components/fireservicerota/translations/es.json index 19ba8da21dd..ddd231ce700 100644 --- a/homeassistant/components/fireservicerota/translations/es.json +++ b/homeassistant/components/fireservicerota/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "create_entry": { "default": "Autenticado correctamente" diff --git a/homeassistant/components/fjaraskupan/fan.py b/homeassistant/components/fjaraskupan/fan.py index c037966f0ef..c856a94fa07 100644 --- a/homeassistant/components/fjaraskupan/fan.py +++ b/homeassistant/components/fjaraskupan/fan.py @@ -82,11 +82,18 @@ class Fan(CoordinatorEntity[Coordinator], FanEntity): async def async_set_percentage(self, percentage: int) -> None: """Set speed.""" - new_speed = percentage_to_ordered_list_item( - ORDERED_NAMED_FAN_SPEEDS, percentage - ) + + # Proactively update percentage to mange successive increases + self._percentage = percentage + async with self.coordinator.async_connect_and_update() as device: - await device.send_fan_speed(int(new_speed)) + if percentage == 0: + await device.send_command(COMMAND_STOP_FAN) + else: + new_speed = percentage_to_ordered_list_item( + ORDERED_NAMED_FAN_SPEEDS, percentage + ) + await device.send_fan_speed(int(new_speed)) async def async_turn_on( self, diff --git a/homeassistant/components/flexit/climate.py b/homeassistant/components/flexit/climate.py index 8129f063a86..27f2314c1f2 100644 --- a/homeassistant/components/flexit/climate.py +++ b/homeassistant/components/flexit/climate.py @@ -6,21 +6,22 @@ from typing import Any import voluptuous as vol -from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( + PLATFORM_SCHEMA, + ClimateEntity, ClimateEntityFeature, HVACAction, HVACMode, ) -from homeassistant.components.modbus import get_hub -from homeassistant.components.modbus.const import ( +from homeassistant.components.modbus import ( CALL_TYPE_REGISTER_HOLDING, CALL_TYPE_REGISTER_INPUT, CALL_TYPE_WRITE_REGISTER, CONF_HUB, DEFAULT_HUB, + ModbusHub, + get_hub, ) -from homeassistant.components.modbus.modbus import ModbusHub from homeassistant.const import ( ATTR_TEMPERATURE, CONF_NAME, diff --git a/homeassistant/components/flo/sensor.py b/homeassistant/components/flo/sensor.py index e7fbd293bd1..9f793b749e4 100644 --- a/homeassistant/components/flo/sensor.py +++ b/homeassistant/components/flo/sensor.py @@ -67,6 +67,7 @@ async def async_setup_entry( class FloDailyUsageSensor(FloEntity, SensorEntity): """Monitors the daily water usage.""" + _attr_device_class = SensorDeviceClass.VOLUME _attr_icon = WATER_ICON _attr_native_unit_of_measurement = VOLUME_GALLONS _attr_state_class: SensorStateClass = SensorStateClass.TOTAL_INCREASING diff --git a/homeassistant/components/flume/sensor.py b/homeassistant/components/flume/sensor.py index b9b5f819520..6d68058732d 100644 --- a/homeassistant/components/flume/sensor.py +++ b/homeassistant/components/flume/sensor.py @@ -40,7 +40,10 @@ async def async_setup_entry( flume_entity_list = [] for device in flume_devices.device_list: - if device[KEY_DEVICE_TYPE] != FLUME_TYPE_SENSOR: + if ( + device[KEY_DEVICE_TYPE] != FLUME_TYPE_SENSOR + or KEY_DEVICE_LOCATION not in device + ): continue device_id = device[KEY_DEVICE_ID] diff --git a/homeassistant/components/flume/translations/cs.json b/homeassistant/components/flume/translations/cs.json index e3f6cf1d39e..52118306733 100644 --- a/homeassistant/components/flume/translations/cs.json +++ b/homeassistant/components/flume/translations/cs.json @@ -10,6 +10,11 @@ "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, "step": { + "reauth_confirm": { + "data": { + "password": "Heslo" + } + }, "user": { "data": { "client_id": "ID klienta", diff --git a/homeassistant/components/flume/translations/es.json b/homeassistant/components/flume/translations/es.json index 5de43c8dcd1..40f22bccd94 100644 --- a/homeassistant/components/flume/translations/es.json +++ b/homeassistant/components/flume/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "error": { "cannot_connect": "No se pudo conectar", diff --git a/homeassistant/components/flunearyou/__init__.py b/homeassistant/components/flunearyou/__init__.py deleted file mode 100644 index ecdf05bbeb1..00000000000 --- a/homeassistant/components/flunearyou/__init__.py +++ /dev/null @@ -1,89 +0,0 @@ -"""The flunearyou component.""" -from __future__ import annotations - -import asyncio -from datetime import timedelta -from functools import partial -from typing import Any - -from pyflunearyou import Client -from pyflunearyou.errors import FluNearYouError - -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, Platform -from homeassistant.core import HomeAssistant -from homeassistant.helpers import aiohttp_client, config_validation as cv -from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed - -from .const import CATEGORY_CDC_REPORT, CATEGORY_USER_REPORT, DOMAIN, LOGGER - -DEFAULT_UPDATE_INTERVAL = timedelta(minutes=30) - -CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) - -PLATFORMS = [Platform.SENSOR] - - -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Set up Flu Near You as config entry.""" - async_create_issue( - hass, - DOMAIN, - "integration_removal", - is_fixable=True, - severity=IssueSeverity.ERROR, - translation_key="integration_removal", - ) - - websession = aiohttp_client.async_get_clientsession(hass) - client = Client(session=websession) - - latitude = entry.data.get(CONF_LATITUDE, hass.config.latitude) - longitude = entry.data.get(CONF_LONGITUDE, hass.config.longitude) - - async def async_update(api_category: str) -> dict[str, Any]: - """Get updated date from the API based on category.""" - try: - if api_category == CATEGORY_CDC_REPORT: - data = await client.cdc_reports.status_by_coordinates( - latitude, longitude - ) - else: - data = await client.user_reports.status_by_coordinates( - latitude, longitude - ) - except FluNearYouError as err: - raise UpdateFailed(err) from err - - return data - - coordinators = {} - data_init_tasks = [] - - for api_category in (CATEGORY_CDC_REPORT, CATEGORY_USER_REPORT): - coordinator = coordinators[api_category] = DataUpdateCoordinator( - hass, - LOGGER, - name=f"{api_category} ({latitude}, {longitude})", - update_interval=DEFAULT_UPDATE_INTERVAL, - update_method=partial(async_update, api_category), - ) - data_init_tasks.append(coordinator.async_config_entry_first_refresh()) - - await asyncio.gather(*data_init_tasks) - hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][entry.entry_id] = coordinators - - await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) - - return True - - -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Unload an Flu Near You config entry.""" - unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - if unload_ok: - hass.data[DOMAIN].pop(entry.entry_id) - - return unload_ok diff --git a/homeassistant/components/flunearyou/config_flow.py b/homeassistant/components/flunearyou/config_flow.py deleted file mode 100644 index 0005e0c257a..00000000000 --- a/homeassistant/components/flunearyou/config_flow.py +++ /dev/null @@ -1,60 +0,0 @@ -"""Define a config flow manager for flunearyou.""" -from __future__ import annotations - -from typing import Any - -from pyflunearyou import Client -from pyflunearyou.errors import FluNearYouError -import voluptuous as vol - -from homeassistant import config_entries -from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE -from homeassistant.data_entry_flow import FlowResult -from homeassistant.helpers import aiohttp_client, config_validation as cv - -from .const import DOMAIN, LOGGER - - -class FluNearYouFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): - """Handle an FluNearYou config flow.""" - - VERSION = 1 - - @property - def data_schema(self) -> vol.Schema: - """Return the data schema for integration.""" - return vol.Schema( - { - vol.Required( - CONF_LATITUDE, default=self.hass.config.latitude - ): cv.latitude, - vol.Required( - CONF_LONGITUDE, default=self.hass.config.longitude - ): cv.longitude, - } - ) - - async def async_step_user( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: - """Handle the start of the config flow.""" - if not user_input: - return self.async_show_form(step_id="user", data_schema=self.data_schema) - - unique_id = f"{user_input[CONF_LATITUDE]}, {user_input[CONF_LONGITUDE]}" - - await self.async_set_unique_id(unique_id) - self._abort_if_unique_id_configured() - - websession = aiohttp_client.async_get_clientsession(self.hass) - client = Client(session=websession) - - try: - await client.cdc_reports.status_by_coordinates( - user_input[CONF_LATITUDE], user_input[CONF_LONGITUDE] - ) - except FluNearYouError as err: - LOGGER.error("Error while configuring integration: %s", err) - return self.async_show_form(step_id="user", errors={"base": "unknown"}) - - return self.async_create_entry(title=unique_id, data=user_input) diff --git a/homeassistant/components/flunearyou/const.py b/homeassistant/components/flunearyou/const.py deleted file mode 100644 index dc9ac629d92..00000000000 --- a/homeassistant/components/flunearyou/const.py +++ /dev/null @@ -1,8 +0,0 @@ -"""Define flunearyou constants.""" -import logging - -DOMAIN = "flunearyou" -LOGGER = logging.getLogger(__package__) - -CATEGORY_CDC_REPORT = "cdc_report" -CATEGORY_USER_REPORT = "user_report" diff --git a/homeassistant/components/flunearyou/diagnostics.py b/homeassistant/components/flunearyou/diagnostics.py deleted file mode 100644 index 1f7812a7403..00000000000 --- a/homeassistant/components/flunearyou/diagnostics.py +++ /dev/null @@ -1,32 +0,0 @@ -"""Diagnostics support for Flu Near You.""" -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 -from homeassistant.core import HomeAssistant -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator - -from .const import CATEGORY_CDC_REPORT, CATEGORY_USER_REPORT, DOMAIN - -TO_REDACT = { - CONF_LATITUDE, - CONF_LONGITUDE, -} - - -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] - - return async_redact_data( - { - CATEGORY_CDC_REPORT: coordinators[CATEGORY_CDC_REPORT].data, - CATEGORY_USER_REPORT: coordinators[CATEGORY_USER_REPORT].data, - }, - TO_REDACT, - ) diff --git a/homeassistant/components/flunearyou/manifest.json b/homeassistant/components/flunearyou/manifest.json deleted file mode 100644 index ee69961d1b0..00000000000 --- a/homeassistant/components/flunearyou/manifest.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "domain": "flunearyou", - "name": "Flu Near You", - "config_flow": true, - "documentation": "https://www.home-assistant.io/integrations/flunearyou", - "requirements": ["pyflunearyou==2.0.2"], - "codeowners": ["@bachya"], - "iot_class": "cloud_polling", - "loggers": ["pyflunearyou"] -} diff --git a/homeassistant/components/flunearyou/repairs.py b/homeassistant/components/flunearyou/repairs.py deleted file mode 100644 index df81a1ae576..00000000000 --- a/homeassistant/components/flunearyou/repairs.py +++ /dev/null @@ -1,44 +0,0 @@ -"""Repairs platform for the Flu Near You integration.""" -from __future__ import annotations - -import asyncio - -import voluptuous as vol - -from homeassistant import data_entry_flow -from homeassistant.components.repairs import RepairsFlow -from homeassistant.core import HomeAssistant - -from .const import DOMAIN - - -class FluNearYouFixFlow(RepairsFlow): - """Handler for an issue fixing flow.""" - - async def async_step_init( - self, user_input: dict[str, str] | None = None - ) -> data_entry_flow.FlowResult: - """Handle the first step of a fix flow.""" - return await self.async_step_confirm() - - async def async_step_confirm( - self, user_input: dict[str, str] | None = None - ) -> data_entry_flow.FlowResult: - """Handle the confirm step of a fix flow.""" - if user_input is not None: - removal_tasks = [ - self.hass.config_entries.async_remove(entry.entry_id) - for entry in self.hass.config_entries.async_entries(DOMAIN) - ] - await asyncio.gather(*removal_tasks) - return self.async_create_entry(title="Fixed issue", data={}) - return self.async_show_form(step_id="confirm", data_schema=vol.Schema({})) - - -async def async_create_fix_flow( - hass: HomeAssistant, - issue_id: str, - data: dict[str, str | int | float | None] | None, -) -> RepairsFlow: - """Create flow.""" - return FluNearYouFixFlow() diff --git a/homeassistant/components/flunearyou/sensor.py b/homeassistant/components/flunearyou/sensor.py deleted file mode 100644 index f666e7412eb..00000000000 --- a/homeassistant/components/flunearyou/sensor.py +++ /dev/null @@ -1,221 +0,0 @@ -"""Support for user- and CDC-based flu info sensors from Flu Near You.""" -from __future__ import annotations - -from collections.abc import Mapping -from typing import Any, Union, cast - -from homeassistant.components.sensor import ( - SensorEntity, - SensorEntityDescription, - SensorStateClass, -) -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_STATE, CONF_LATITUDE, CONF_LONGITUDE -from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import StateType -from homeassistant.helpers.update_coordinator import ( - CoordinatorEntity, - DataUpdateCoordinator, -) - -from .const import CATEGORY_CDC_REPORT, CATEGORY_USER_REPORT, DOMAIN - -ATTR_CITY = "city" -ATTR_REPORTED_DATE = "reported_date" -ATTR_REPORTED_LATITUDE = "reported_latitude" -ATTR_REPORTED_LONGITUDE = "reported_longitude" -ATTR_STATE_REPORTS_LAST_WEEK = "state_reports_last_week" -ATTR_STATE_REPORTS_THIS_WEEK = "state_reports_this_week" -ATTR_ZIP_CODE = "zip_code" - -SENSOR_TYPE_CDC_LEVEL = "level" -SENSOR_TYPE_CDC_LEVEL2 = "level2" -SENSOR_TYPE_USER_CHICK = "chick" -SENSOR_TYPE_USER_DENGUE = "dengue" -SENSOR_TYPE_USER_FLU = "flu" -SENSOR_TYPE_USER_LEPTO = "lepto" -SENSOR_TYPE_USER_NO_SYMPTOMS = "none" -SENSOR_TYPE_USER_SYMPTOMS = "symptoms" -SENSOR_TYPE_USER_TOTAL = "total" - -CDC_SENSOR_DESCRIPTIONS = ( - SensorEntityDescription( - key=SENSOR_TYPE_CDC_LEVEL, - name="CDC level", - icon="mdi:biohazard", - ), - SensorEntityDescription( - key=SENSOR_TYPE_CDC_LEVEL2, - name="CDC level 2", - icon="mdi:biohazard", - ), -) - -USER_SENSOR_DESCRIPTIONS = ( - SensorEntityDescription( - key=SENSOR_TYPE_USER_CHICK, - name="Avian flu symptoms", - icon="mdi:alert", - native_unit_of_measurement="reports", - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key=SENSOR_TYPE_USER_DENGUE, - name="Dengue fever symptoms", - icon="mdi:alert", - native_unit_of_measurement="reports", - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key=SENSOR_TYPE_USER_FLU, - name="Flu symptoms", - icon="mdi:alert", - native_unit_of_measurement="reports", - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key=SENSOR_TYPE_USER_LEPTO, - name="Leptospirosis symptoms", - icon="mdi:alert", - native_unit_of_measurement="reports", - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key=SENSOR_TYPE_USER_NO_SYMPTOMS, - name="No symptoms", - icon="mdi:alert", - native_unit_of_measurement="reports", - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key=SENSOR_TYPE_USER_SYMPTOMS, - name="Flu-like symptoms", - icon="mdi:alert", - native_unit_of_measurement="reports", - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key=SENSOR_TYPE_USER_TOTAL, - name="Total symptoms", - icon="mdi:alert", - native_unit_of_measurement="reports", - state_class=SensorStateClass.MEASUREMENT, - ), -) - -EXTENDED_SENSOR_TYPE_MAPPING = { - SENSOR_TYPE_USER_FLU: "ili", - SENSOR_TYPE_USER_NO_SYMPTOMS: "no_symptoms", - SENSOR_TYPE_USER_TOTAL: "total_surveys", -} - - -async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback -) -> None: - """Set up Flu Near You sensors based on a config entry.""" - coordinators = hass.data[DOMAIN][entry.entry_id] - - sensors: list[CdcSensor | UserSensor] = [ - CdcSensor(coordinators[CATEGORY_CDC_REPORT], entry, description) - for description in CDC_SENSOR_DESCRIPTIONS - ] - sensors.extend( - [ - UserSensor(coordinators[CATEGORY_USER_REPORT], entry, description) - for description in USER_SENSOR_DESCRIPTIONS - ] - ) - async_add_entities(sensors) - - -class FluNearYouSensor(CoordinatorEntity, SensorEntity): - """Define a base Flu Near You sensor.""" - - _attr_has_entity_name = True - - def __init__( - self, - coordinator: DataUpdateCoordinator, - entry: ConfigEntry, - description: SensorEntityDescription, - ) -> None: - """Initialize the sensor.""" - super().__init__(coordinator) - - self._attr_unique_id = ( - f"{entry.data[CONF_LATITUDE]}," - f"{entry.data[CONF_LONGITUDE]}_{description.key}" - ) - self._entry = entry - self.entity_description = description - - -class CdcSensor(FluNearYouSensor): - """Define a sensor for CDC reports.""" - - @property - def extra_state_attributes(self) -> Mapping[str, Any] | None: - """Return entity specific state attributes.""" - return { - ATTR_REPORTED_DATE: self.coordinator.data["week_date"], - ATTR_STATE: self.coordinator.data["name"], - } - - @property - def native_value(self) -> StateType: - """Return the value reported by the sensor.""" - return cast( - Union[str, None], self.coordinator.data[self.entity_description.key] - ) - - -class UserSensor(FluNearYouSensor): - """Define a sensor for user reports.""" - - @property - def extra_state_attributes(self) -> Mapping[str, Any] | None: - """Return entity specific state attributes.""" - attrs = { - ATTR_CITY: self.coordinator.data["local"]["city"].split("(")[0], - ATTR_REPORTED_LATITUDE: self.coordinator.data["local"]["latitude"], - ATTR_REPORTED_LONGITUDE: self.coordinator.data["local"]["longitude"], - ATTR_STATE: self.coordinator.data["state"]["name"], - ATTR_ZIP_CODE: self.coordinator.data["local"]["zip"], - } - - if self.entity_description.key in self.coordinator.data["state"]["data"]: - states_key = self.entity_description.key - elif self.entity_description.key in EXTENDED_SENSOR_TYPE_MAPPING: - states_key = EXTENDED_SENSOR_TYPE_MAPPING[self.entity_description.key] - - attrs[ATTR_STATE_REPORTS_THIS_WEEK] = self.coordinator.data["state"]["data"][ - states_key - ] - attrs[ATTR_STATE_REPORTS_LAST_WEEK] = self.coordinator.data["state"][ - "last_week_data" - ][states_key] - - return attrs - - @property - def native_value(self) -> StateType: - """Return the value reported by the sensor.""" - if self.entity_description.key == SENSOR_TYPE_USER_TOTAL: - value = sum( - v - for k, v in self.coordinator.data["local"].items() - if k - in ( - SENSOR_TYPE_USER_CHICK, - SENSOR_TYPE_USER_DENGUE, - SENSOR_TYPE_USER_FLU, - SENSOR_TYPE_USER_LEPTO, - SENSOR_TYPE_USER_SYMPTOMS, - ) - ) - else: - value = self.coordinator.data["local"][self.entity_description.key] - - return cast(int, value) diff --git a/homeassistant/components/flunearyou/strings.json b/homeassistant/components/flunearyou/strings.json deleted file mode 100644 index 59ec6125a34..00000000000 --- a/homeassistant/components/flunearyou/strings.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "config": { - "step": { - "user": { - "title": "Configure Flu Near You", - "description": "Monitor user-based and CDC reports for a pair of coordinates.", - "data": { - "latitude": "[%key:common::config_flow::data::latitude%]", - "longitude": "[%key:common::config_flow::data::longitude%]" - } - } - }, - "error": { - "unknown": "[%key:common::config_flow::error::unknown%]" - }, - "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_location%]" - } - }, - "issues": { - "integration_removal": { - "title": "Flu Near You is no longer available", - "fix_flow": { - "step": { - "confirm": { - "title": "Remove Flu Near You", - "description": "The external data source powering the Flu Near You integration is no longer available; thus, the integration no longer works.\n\nPress SUBMIT to remove Flu Near You from your Home Assistant instance." - } - } - } - } - } -} diff --git a/homeassistant/components/flunearyou/translations/bg.json b/homeassistant/components/flunearyou/translations/bg.json deleted file mode 100644 index 360abac2642..00000000000 --- a/homeassistant/components/flunearyou/translations/bg.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" - }, - "step": { - "user": { - "data": { - "latitude": "\u0413\u0435\u043e\u0433\u0440\u0430\u0444\u0441\u043a\u0430 \u0448\u0438\u0440\u0438\u043d\u0430" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/ca.json b/homeassistant/components/flunearyou/translations/ca.json deleted file mode 100644 index 9c4e55f8b54..00000000000 --- a/homeassistant/components/flunearyou/translations/ca.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "La ubicaci\u00f3 ja est\u00e0 configurada" - }, - "error": { - "unknown": "Error inesperat" - }, - "step": { - "user": { - "data": { - "latitude": "Latitud", - "longitude": "Longitud" - }, - "description": "Monitoritza informes basats en usuari i CDC per a parells de coordenades.", - "title": "Configuraci\u00f3 Flu Near You" - } - } - }, - "issues": { - "integration_removal": { - "fix_flow": { - "step": { - "confirm": { - "description": "La font de dades externa que alimenta la integraci\u00f3 Flu Near You ja no est\u00e0 disponible; per tant, la integraci\u00f3 ja no funciona. \n\nPrem ENVIAR per eliminar Flu Near You de Home Assistant.", - "title": "Elimina Flu Near You" - } - } - }, - "title": "Flu Near You ja no est\u00e0 disponible" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/cs.json b/homeassistant/components/flunearyou/translations/cs.json deleted file mode 100644 index 64cb764a52d..00000000000 --- a/homeassistant/components/flunearyou/translations/cs.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Um\u00edst\u011bn\u00ed je ji\u017e nastaveno" - }, - "error": { - "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" - }, - "step": { - "user": { - "data": { - "latitude": "Zem\u011bpisn\u00e1 \u0161\u00ed\u0159ka", - "longitude": "Zem\u011bpisn\u00e1 d\u00e9lka" - }, - "title": "Nastaven\u00ed Flu Near You" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/de.json b/homeassistant/components/flunearyou/translations/de.json deleted file mode 100644 index 4e2287ad9ca..00000000000 --- a/homeassistant/components/flunearyou/translations/de.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Standort ist bereits konfiguriert" - }, - "error": { - "unknown": "Unerwarteter Fehler" - }, - "step": { - "user": { - "data": { - "latitude": "Breitengrad", - "longitude": "L\u00e4ngengrad" - }, - "description": "\u00dcberwache benutzerbasierte und CDC-Berichte f\u00fcr ein Koordinatenpaar.", - "title": "Konfiguriere Grippe in deiner N\u00e4he" - } - } - }, - "issues": { - "integration_removal": { - "fix_flow": { - "step": { - "confirm": { - "description": "Die externe Datenquelle, aus der die Integration von Flu Near You gespeist wird, ist nicht mehr verf\u00fcgbar; daher funktioniert die Integration nicht mehr.\n\nDr\u00fccke SENDEN, um Flu Near You aus deiner Home Assistant-Instanz zu entfernen.", - "title": "Flu Near You entfernen" - } - } - }, - "title": "Flu Near You ist nicht mehr verf\u00fcgbar" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/el.json b/homeassistant/components/flunearyou/translations/el.json deleted file mode 100644 index ca6fae97190..00000000000 --- a/homeassistant/components/flunearyou/translations/el.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "\u0397 \u03c4\u03bf\u03c0\u03bf\u03b8\u03b5\u03c3\u03af\u03b1 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af" - }, - "error": { - "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" - }, - "step": { - "user": { - "data": { - "latitude": "\u0393\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03b9\u03ba\u03cc \u03c0\u03bb\u03ac\u03c4\u03bf\u03c2", - "longitude": "\u0393\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03b9\u03ba\u03cc \u03bc\u03ae\u03ba\u03bf\u03c2" - }, - "description": "\u03a0\u03b1\u03c1\u03b1\u03ba\u03bf\u03bb\u03bf\u03cd\u03b8\u03b7\u03c3\u03b7 \u03b1\u03bd\u03b1\u03c6\u03bf\u03c1\u03ce\u03bd \u03b2\u03ac\u03c3\u03b5\u03b9 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7 \u03ba\u03b1\u03b9 CDC \u03b3\u03b9\u03b1 \u03ad\u03bd\u03b1 \u03b6\u03b5\u03cd\u03b3\u03bf\u03c2 \u03c3\u03c5\u03bd\u03c4\u03b5\u03c4\u03b1\u03b3\u03bc\u03ad\u03bd\u03c9\u03bd.", - "title": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Flu Near You" - } - } - }, - "issues": { - "integration_removal": { - "fix_flow": { - "step": { - "confirm": { - "description": "\u0397 \u03b5\u03be\u03c9\u03c4\u03b5\u03c1\u03b9\u03ba\u03ae \u03c0\u03b7\u03b3\u03ae \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03c9\u03bd \u03c0\u03bf\u03c5 \u03c4\u03c1\u03bf\u03c6\u03bf\u03b4\u03bf\u03c4\u03b5\u03af \u03c4\u03b7\u03bd \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 Flu Near You \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03c0\u03bb\u03ad\u03bf\u03bd \u03b4\u03b9\u03b1\u03b8\u03ad\u03c3\u03b9\u03bc\u03b7. \u0395\u03c0\u03bf\u03bc\u03ad\u03bd\u03c9\u03c2, \u03b7 \u03b5\u03bd\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03b4\u03b5\u03bd \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03b5\u03af \u03c0\u03bb\u03ad\u03bf\u03bd. \n\n \u03a0\u03b1\u03c4\u03ae\u03c3\u03c4\u03b5 SUBMIT \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b1\u03c6\u03b1\u03b9\u03c1\u03ad\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03b3\u03c1\u03af\u03c0\u03b7 \u03ba\u03bf\u03bd\u03c4\u03ac \u03c3\u03b1\u03c2 \u03b1\u03c0\u03cc \u03c4\u03b7\u03bd \u03c0\u03b1\u03c1\u03bf\u03c5\u03c3\u03af\u03b1 \u03c4\u03bf\u03c5 Home Assistant.", - "title": "\u0391\u03c6\u03b1\u03b9\u03c1\u03ad\u03c3\u03c4\u03b5 \u03c4\u03bf Flu Near You" - } - } - }, - "title": "\u03a4\u03bf Flu Near You \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03c0\u03bb\u03ad\u03bf\u03bd \u03b4\u03b9\u03b1\u03b8\u03ad\u03c3\u03b9\u03bc\u03bf" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/en.json b/homeassistant/components/flunearyou/translations/en.json deleted file mode 100644 index 7e76b54b18a..00000000000 --- a/homeassistant/components/flunearyou/translations/en.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Location is already configured" - }, - "error": { - "unknown": "Unexpected error" - }, - "step": { - "user": { - "data": { - "latitude": "Latitude", - "longitude": "Longitude" - }, - "description": "Monitor user-based and CDC reports for a pair of coordinates.", - "title": "Configure Flu Near You" - } - } - }, - "issues": { - "integration_removal": { - "fix_flow": { - "step": { - "confirm": { - "description": "The external data source powering the Flu Near You integration is no longer available; thus, the integration no longer works.\n\nPress SUBMIT to remove Flu Near You from your Home Assistant instance.", - "title": "Remove Flu Near You" - } - } - }, - "title": "Flu Near You is no longer available" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/es-419.json b/homeassistant/components/flunearyou/translations/es-419.json deleted file mode 100644 index 726a898a8b6..00000000000 --- a/homeassistant/components/flunearyou/translations/es-419.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Estas coordenadas ya est\u00e1n registradas." - }, - "step": { - "user": { - "data": { - "latitude": "Latitud", - "longitude": "Longitud" - }, - "description": "Monitoree los repotes basados en el usuario y los CDC para un par de coordenadas.", - "title": "Configurar Flu Near You (Gripe cerca de usted)" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/es.json b/homeassistant/components/flunearyou/translations/es.json deleted file mode 100644 index a7d9cf89f6e..00000000000 --- a/homeassistant/components/flunearyou/translations/es.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "La ubicaci\u00f3n ya est\u00e1 configurada" - }, - "error": { - "unknown": "Error inesperado" - }, - "step": { - "user": { - "data": { - "latitude": "Latitud", - "longitude": "Longitud" - }, - "description": "Supervisa los informes del CDC y los basados en los usuarios para un par de coordenadas.", - "title": "Configurar Flu Near You" - } - } - }, - "issues": { - "integration_removal": { - "fix_flow": { - "step": { - "confirm": { - "description": "La fuente de datos externa que alimenta la integraci\u00f3n Flu Near You ya no est\u00e1 disponible; por lo tanto, la integraci\u00f3n ya no funciona. \n\nPulsa ENVIAR para eliminar Flu Near You de tu instancia Home Assistant.", - "title": "Eliminar Flu Near You" - } - } - }, - "title": "Flu Near You ya no est\u00e1 disponible" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/et.json b/homeassistant/components/flunearyou/translations/et.json deleted file mode 100644 index 447ad54c25a..00000000000 --- a/homeassistant/components/flunearyou/translations/et.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Asukoht on juba h\u00e4\u00e4lestatud" - }, - "error": { - "unknown": "Tundmatu viga" - }, - "step": { - "user": { - "data": { - "latitude": "Laiuskraad", - "longitude": "Pikkuskraad" - }, - "description": "Kasutajap\u00f5histe ja CDC-aruannete j\u00e4lgimine antud asukohas.", - "title": "Seadista Flu Near You" - } - } - }, - "issues": { - "integration_removal": { - "fix_flow": { - "step": { - "confirm": { - "description": "Flu Near You integratsiooni k\u00e4ivitav v\u00e4line andmeallikas ei ole enam saadaval; seega ei t\u00f6\u00f6ta integratsioon enam.\n\nVajutage SUBMIT, et eemaldada Flu Near You oma Home Assistant'i instantsist.", - "title": "Eemalda Flu Near You" - } - } - }, - "title": "Flu Near You pole enam saadaval" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/fi.json b/homeassistant/components/flunearyou/translations/fi.json deleted file mode 100644 index b751fda5e4c..00000000000 --- a/homeassistant/components/flunearyou/translations/fi.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "config": { - "error": { - "unknown": "Odottamaton virhe" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/fr.json b/homeassistant/components/flunearyou/translations/fr.json deleted file mode 100644 index ebc2ccc385a..00000000000 --- a/homeassistant/components/flunearyou/translations/fr.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "L'emplacement est d\u00e9j\u00e0 configur\u00e9" - }, - "error": { - "unknown": "Erreur inattendue" - }, - "step": { - "user": { - "data": { - "latitude": "Latitude", - "longitude": "Longitude" - }, - "description": "Surveillez les rapports des utilisateurs et du CDC pour des coordonn\u00e9es.", - "title": "Configurer Flu Near You" - } - } - }, - "issues": { - "integration_removal": { - "title": "Flu Near You n'est plus disponible" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/hu.json b/homeassistant/components/flunearyou/translations/hu.json deleted file mode 100644 index efda42723b4..00000000000 --- a/homeassistant/components/flunearyou/translations/hu.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "A hely m\u00e1r konfigur\u00e1lva van" - }, - "error": { - "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" - }, - "step": { - "user": { - "data": { - "latitude": "Sz\u00e9less\u00e9g", - "longitude": "Hossz\u00fas\u00e1g" - }, - "description": "Figyelje a felhaszn\u00e1l\u00f3alap\u00fa \u00e9s a CDC jelent\u00e9seket egy p\u00e1r koordin\u00e1t\u00e1ra.", - "title": "Flu Near You weboldal konfigur\u00e1l\u00e1sa" - } - } - }, - "issues": { - "integration_removal": { - "fix_flow": { - "step": { - "confirm": { - "description": "A Flu Near You integr\u00e1ci\u00f3t m\u0171k\u00f6dtet\u0151 k\u00fcls\u0151 adatforr\u00e1s m\u00e1r nem el\u00e9rhet\u0151, \u00edgy az integr\u00e1ci\u00f3 m\u00e1r nem m\u0171k\u00f6dik.\n\nNyomja meg a MEHET gombot a Flu Near You elt\u00e1vol\u00edt\u00e1s\u00e1hoz a Home Assistantb\u00f3l.", - "title": "Flu Near You elt\u00e1vol\u00edt\u00e1sa" - } - } - }, - "title": "Flu Near You m\u00e1r nem el\u00e9rhet\u0151" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/id.json b/homeassistant/components/flunearyou/translations/id.json deleted file mode 100644 index 72fcfefc78d..00000000000 --- a/homeassistant/components/flunearyou/translations/id.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Lokasi sudah dikonfigurasi" - }, - "error": { - "unknown": "Kesalahan yang tidak diharapkan" - }, - "step": { - "user": { - "data": { - "latitude": "Lintang", - "longitude": "Bujur" - }, - "description": "Pantau laporan berbasis pengguna dan CDC berdasarkan data koordinat.", - "title": "Konfigurasikan Flu Near You" - } - } - }, - "issues": { - "integration_removal": { - "fix_flow": { - "step": { - "confirm": { - "description": "Sumber data eksternal yang mendukung integrasi Flu Near You tidak lagi tersedia, sehingga integrasi tidak lagi berfungsi. \n\nTekan KIRIM untuk menghapus integrasi Flu Near You dari instans Home Assistant Anda.", - "title": "Hapus Integrasi Flu Near You" - } - } - }, - "title": "Integrasi Flu Year You tidak lagi tersedia" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/it.json b/homeassistant/components/flunearyou/translations/it.json deleted file mode 100644 index a8939f2d18c..00000000000 --- a/homeassistant/components/flunearyou/translations/it.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "La posizione \u00e8 gi\u00e0 configurata" - }, - "error": { - "unknown": "Errore imprevisto" - }, - "step": { - "user": { - "data": { - "latitude": "Latitudine", - "longitude": "Logitudine" - }, - "description": "Monitorare i rapporti basati su utenti e quelli della CDC per una coppia di coordinate.", - "title": "Configurare Flu Near You" - } - } - }, - "issues": { - "integration_removal": { - "fix_flow": { - "step": { - "confirm": { - "description": "L'origine dati esterna che alimenta l'integrazione Flu Near You non \u00e8 pi\u00f9 disponibile; quindi, l'integrazione non funziona pi\u00f9. \n\nPremi INVIA per rimuovere Flu Near You dall'istanza di Home Assistant.", - "title": "Rimuovi Flu Near You" - } - } - }, - "title": "Flu Near You non \u00e8 pi\u00f9 disponibile" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/ja.json b/homeassistant/components/flunearyou/translations/ja.json deleted file mode 100644 index 06cf83e27be..00000000000 --- a/homeassistant/components/flunearyou/translations/ja.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "\u30ed\u30b1\u30fc\u30b7\u30e7\u30f3\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" - }, - "error": { - "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" - }, - "step": { - "user": { - "data": { - "latitude": "\u7def\u5ea6", - "longitude": "\u7d4c\u5ea6" - }, - "description": "\u30e6\u30fc\u30b6\u30fc\u30d9\u30fc\u30b9\u306e\u30ec\u30dd\u30fc\u30c8\u3068CDC\u306e\u30ec\u30dd\u30fc\u30c8\u3092\u30da\u30a2\u306b\u3057\u3066\u5ea7\u6a19\u3067\u30e2\u30cb\u30bf\u30fc\u3057\u307e\u3059\u3002", - "title": "\u8fd1\u304f\u306eFlu\u3092\u8a2d\u5b9a" - } - } - }, - "issues": { - "integration_removal": { - "fix_flow": { - "step": { - "confirm": { - "description": "Flu Near You\u3068\u306e\u7d71\u5408\u306b\u5fc5\u8981\u306a\u5916\u90e8\u30c7\u30fc\u30bf\u30bd\u30fc\u30b9\u304c\u4f7f\u7528\u3067\u304d\u306a\u304f\u306a\u3063\u305f\u305f\u3081\u3001\u7d71\u5408\u306f\u6a5f\u80fd\u3057\u306a\u304f\u306a\u308a\u307e\u3057\u305f\u3002\n\nSUBMIT\u3092\u62bc\u3057\u3066\u3001Flu Near You\u3092Home Assistant\u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u304b\u3089\u524a\u9664\u3057\u307e\u3059\u3002", - "title": "\u8fd1\u304f\u306eFlu Near You\u3092\u524a\u9664" - } - } - }, - "title": "Flu Near You\u306f\u3001\u5229\u7528\u3067\u304d\u306a\u304f\u306a\u308a\u307e\u3057\u305f" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/ko.json b/homeassistant/components/flunearyou/translations/ko.json deleted file mode 100644 index bfe1945fa67..00000000000 --- a/homeassistant/components/flunearyou/translations/ko.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "\uc704\uce58\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" - }, - "error": { - "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" - }, - "step": { - "user": { - "data": { - "latitude": "\uc704\ub3c4", - "longitude": "\uacbd\ub3c4" - }, - "description": "\uc0ac\uc6a9\uc790 \uae30\ubc18 \ub370\uc774\ud130 \ubc0f CDC \ubcf4\uace0\uc11c\uc5d0\uc11c \uc88c\ud45c\ub97c \ubaa8\ub2c8\ud130\ub9c1\ud569\ub2c8\ub2e4.", - "title": "Flu Near You \uad6c\uc131\ud558\uae30" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/lb.json b/homeassistant/components/flunearyou/translations/lb.json deleted file mode 100644 index 457f64f34a7..00000000000 --- a/homeassistant/components/flunearyou/translations/lb.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Standuert ass scho konfigur\u00e9iert" - }, - "error": { - "unknown": "Onerwaarte Feeler" - }, - "step": { - "user": { - "data": { - "latitude": "Breedegrad", - "longitude": "L\u00e4ngegrad" - }, - "description": "Iwwerwach Benotzer-bas\u00e9iert an CDC Berichter fir Koordinaten.", - "title": "Flu Near You konfigur\u00e9ieren" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/nl.json b/homeassistant/components/flunearyou/translations/nl.json deleted file mode 100644 index 0938bd45206..00000000000 --- a/homeassistant/components/flunearyou/translations/nl.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Locatie is al geconfigureerd" - }, - "error": { - "unknown": "Onverwachte fout" - }, - "step": { - "user": { - "data": { - "latitude": "Breedtegraad", - "longitude": "Lengtegraad" - }, - "description": "Bewaak gebruikersgebaseerde en CDC-rapporten voor een paar co\u00f6rdinaten.", - "title": "Configureer \nFlu Near You" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/no.json b/homeassistant/components/flunearyou/translations/no.json deleted file mode 100644 index 898889bc348..00000000000 --- a/homeassistant/components/flunearyou/translations/no.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Plasseringen er allerede konfigurert" - }, - "error": { - "unknown": "Uventet feil" - }, - "step": { - "user": { - "data": { - "latitude": "Breddegrad", - "longitude": "Lengdegrad" - }, - "description": "Overv\u00e5k brukerbaserte rapporter og CDC-rapporter for et par koordinater.", - "title": "Konfigurere influensa i n\u00e6rheten av deg" - } - } - }, - "issues": { - "integration_removal": { - "fix_flow": { - "step": { - "confirm": { - "description": "Den eksterne datakilden som driver Flu Near You-integrasjonen er ikke lenger tilgjengelig; dermed fungerer ikke integreringen lenger. \n\n Trykk SUBMIT for \u00e5 fjerne Flu Near You fra Home Assistant-forekomsten.", - "title": "Fjern influensa n\u00e6r deg" - } - } - }, - "title": "Flu Near You er ikke lenger tilgjengelig" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/pl.json b/homeassistant/components/flunearyou/translations/pl.json deleted file mode 100644 index 71d390992e4..00000000000 --- a/homeassistant/components/flunearyou/translations/pl.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Lokalizacja jest ju\u017c skonfigurowana" - }, - "error": { - "unknown": "Nieoczekiwany b\u0142\u0105d" - }, - "step": { - "user": { - "data": { - "latitude": "Szeroko\u015b\u0107 geograficzna", - "longitude": "D\u0142ugo\u015b\u0107 geograficzna" - }, - "description": "Monitoruj raporty oparte na u\u017cytkownikach i CDC dla pary wsp\u00f3\u0142rz\u0119dnych.", - "title": "Konfiguracja Flu Near You" - } - } - }, - "issues": { - "integration_removal": { - "fix_flow": { - "step": { - "confirm": { - "description": "Zewn\u0119trzne \u017ar\u00f3d\u0142o danych dla integracji Flu Near You nie jest ju\u017c dost\u0119pne; tym sposobem integracja ju\u017c nie dzia\u0142a. \n\nNaci\u015bnij ZATWIERD\u0179, aby usun\u0105\u0107 Flu Near You z Home Assistanta.", - "title": "Usu\u0144 Flu Near You" - } - } - }, - "title": "Flu Near You nie jest ju\u017c dost\u0119pna" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/pt-BR.json b/homeassistant/components/flunearyou/translations/pt-BR.json deleted file mode 100644 index eeca693be89..00000000000 --- a/homeassistant/components/flunearyou/translations/pt-BR.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Localiza\u00e7\u00e3o j\u00e1 est\u00e1 configurada" - }, - "error": { - "unknown": "Erro inesperado" - }, - "step": { - "user": { - "data": { - "latitude": "Latitude", - "longitude": "Longitude" - }, - "description": "Monitore relat\u00f3rios baseados em usu\u00e1rio e CDC para um par de coordenadas.", - "title": "Configurar Flue Near You" - } - } - }, - "issues": { - "integration_removal": { - "fix_flow": { - "step": { - "confirm": { - "description": "A fonte de dados externa que alimenta a integra\u00e7\u00e3o do Flu Near You n\u00e3o est\u00e1 mais dispon\u00edvel; assim, a integra\u00e7\u00e3o n\u00e3o funciona mais. \n\n Pressione ENVIAR para remover Flu Near You da sua inst\u00e2ncia do Home Assistant.", - "title": "Remova Flu Near You" - } - } - }, - "title": "Flu Near You n\u00e3o est\u00e1 mais dispon\u00edvel" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/pt.json b/homeassistant/components/flunearyou/translations/pt.json deleted file mode 100644 index 219446a038d..00000000000 --- a/homeassistant/components/flunearyou/translations/pt.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "A localiza\u00e7\u00e3o j\u00e1 est\u00e1 configurada" - }, - "error": { - "unknown": "Erro inesperado" - }, - "step": { - "user": { - "data": { - "latitude": "Latitude", - "longitude": "Longitude" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/ru.json b/homeassistant/components/flunearyou/translations/ru.json deleted file mode 100644 index d9f184e05a6..00000000000 --- a/homeassistant/components/flunearyou/translations/ru.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "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." - }, - "error": { - "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." - }, - "step": { - "user": { - "data": { - "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430", - "longitude": "\u0414\u043e\u043b\u0433\u043e\u0442\u0430" - }, - "description": "\u041c\u043e\u043d\u0438\u0442\u043e\u0440\u0438\u043d\u0433 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0445 \u0438 CDC \u043e\u0442\u0447\u0435\u0442\u043e\u0432 \u043f\u043e \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u043c \u043a\u043e\u043e\u0440\u0434\u0438\u043d\u0430\u0442\u0430\u043c.", - "title": "Flu Near You" - } - } - }, - "issues": { - "integration_removal": { - "fix_flow": { - "step": { - "confirm": { - "description": "\u0412\u043d\u0435\u0448\u043d\u0438\u0439 \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a \u0434\u0430\u043d\u043d\u044b\u0445, \u043e\u0431\u0435\u0441\u043f\u0435\u0447\u0438\u0432\u0430\u044e\u0449\u0438\u0439 \u0440\u0430\u0431\u043e\u0442\u0443 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 Flu Near You, \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435 \u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d.\n\n\u041d\u0430\u0436\u043c\u0438\u0442\u0435 \u041f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u044c, \u0447\u0442\u043e\u0431\u044b \u0443\u0434\u0430\u043b\u0438\u0442\u044c Flu Near You \u0438\u0437 \u0412\u0430\u0448\u0435\u0433\u043e Home Assistant.", - "title": "\u0423\u0434\u0430\u043b\u0435\u043d\u0438\u0435 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 Flu Near You" - } - } - }, - "title": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f Flu Near You \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/sl.json b/homeassistant/components/flunearyou/translations/sl.json deleted file mode 100644 index 667f0e3c0ed..00000000000 --- a/homeassistant/components/flunearyou/translations/sl.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Te koordinate so \u017ee registrirane." - }, - "step": { - "user": { - "data": { - "latitude": "Zemljepisna \u0161irina", - "longitude": "Zemljepisna dol\u017eina" - }, - "description": "Spremljajte uporabni\u0161ke in CDC obvestila za dolo\u010dene koordinate.", - "title": "Konfigurirajte Flu Near You" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/sv.json b/homeassistant/components/flunearyou/translations/sv.json deleted file mode 100644 index 0ce55468d86..00000000000 --- a/homeassistant/components/flunearyou/translations/sv.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Dessa koordinater \u00e4r redan registrerade." - }, - "error": { - "unknown": "Ov\u00e4ntat fel" - }, - "step": { - "user": { - "data": { - "latitude": "Latitud", - "longitude": "Longitud" - }, - "description": "\u00d6vervaka anv\u00e4ndarbaserade och CDC-rapporter f\u00f6r ett par koordinater.", - "title": "Konfigurera influensa i n\u00e4rheten av dig" - } - } - }, - "issues": { - "integration_removal": { - "fix_flow": { - "step": { - "confirm": { - "description": "Den externa datak\u00e4llan som driver Flu Near You-integrationen \u00e4r inte l\u00e4ngre tillg\u00e4nglig; allts\u00e5 fungerar inte integrationen l\u00e4ngre. \n\n Tryck p\u00e5 SUBMIT f\u00f6r att ta bort Flu Near You fr\u00e5n din Home Assistant-instans.", - "title": "Ta bort influensa n\u00e4ra dig" - } - } - }, - "title": "Influensa n\u00e4ra dig \u00e4r inte l\u00e4ngre tillg\u00e4nglig" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/tr.json b/homeassistant/components/flunearyou/translations/tr.json deleted file mode 100644 index 1e762c75f7a..00000000000 --- a/homeassistant/components/flunearyou/translations/tr.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Konum zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" - }, - "error": { - "unknown": "Beklenmeyen hata" - }, - "step": { - "user": { - "data": { - "latitude": "Enlem", - "longitude": "Boylam" - }, - "description": "Bir \u00e7ift koordinat i\u00e7in kullan\u0131c\u0131 tabanl\u0131 raporlar\u0131 ve CDC raporlar\u0131n\u0131 izleyin.", - "title": "Flu Near You'yu Yap\u0131land\u0131r\u0131n" - } - } - }, - "issues": { - "integration_removal": { - "fix_flow": { - "step": { - "confirm": { - "description": "Flu Near You entegrasyonuna g\u00fc\u00e7 sa\u011flayan harici veri kayna\u011f\u0131 art\u0131k mevcut de\u011fil; bu nedenle, entegrasyon art\u0131k \u00e7al\u0131\u015fm\u0131yor. \n\n Home Assistant \u00f6rne\u011finden Flu Near You'yu kald\u0131rmak i\u00e7in G\u00d6NDER'e bas\u0131n.", - "title": "Yak\u0131n\u0131n\u0131zdaki Flu Near You'yu Kald\u0131r\u0131n" - } - } - }, - "title": "Flu Near You art\u0131k mevcut de\u011fil" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/uk.json b/homeassistant/components/flunearyou/translations/uk.json deleted file mode 100644 index 354a04d8e7a..00000000000 --- a/homeassistant/components/flunearyou/translations/uk.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0434\u043b\u044f \u0446\u044c\u043e\u0433\u043e \u043c\u0456\u0441\u0446\u0435\u0437\u043d\u0430\u0445\u043e\u0434\u0436\u0435\u043d\u043d\u044f \u0432\u0436\u0435 \u0432\u0438\u043a\u043e\u043d\u0430\u043d\u0435." - }, - "error": { - "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" - }, - "step": { - "user": { - "data": { - "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430", - "longitude": "\u0414\u043e\u0432\u0433\u043e\u0442\u0430" - }, - "description": "\u041c\u043e\u043d\u0456\u0442\u043e\u0440\u0438\u043d\u0433 \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0446\u044c\u043a\u0438\u0445 \u0456 CDC \u0437\u0432\u0456\u0442\u0456\u0432 \u0437\u0430 \u0432\u043a\u0430\u0437\u0430\u043d\u0438\u043c\u0438 \u043a\u043e\u043e\u0440\u0434\u0438\u043d\u0430\u0442\u0430\u043c\u0438.", - "title": "Flu Near You" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/zh-Hans.json b/homeassistant/components/flunearyou/translations/zh-Hans.json deleted file mode 100644 index f55159dc235..00000000000 --- a/homeassistant/components/flunearyou/translations/zh-Hans.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "\u4f4d\u7f6e\u5b8c\u6210\u914d\u7f6e" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/zh-Hant.json b/homeassistant/components/flunearyou/translations/zh-Hant.json deleted file mode 100644 index 42b975c451b..00000000000 --- a/homeassistant/components/flunearyou/translations/zh-Hant.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "\u5ea7\u6a19\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" - }, - "error": { - "unknown": "\u672a\u9810\u671f\u932f\u8aa4" - }, - "step": { - "user": { - "data": { - "latitude": "\u7def\u5ea6", - "longitude": "\u7d93\u5ea6" - }, - "description": "\u76e3\u6e2c\u4f7f\u7528\u8005\u8207 CDC \u56de\u5831\u76f8\u5c0d\u61c9\u5ea7\u6a19\u3002", - "title": "\u8a2d\u5b9a Flu Near You" - } - } - }, - "issues": { - "integration_removal": { - "fix_flow": { - "step": { - "confirm": { - "description": "The external data source powering the Flu Near You \u6574\u5408\u6240\u4f7f\u7528\u7684\u5916\u90e8\u8cc7\u6599\u4f86\u6e90\u5df2\u7d93\u7121\u6cd5\u4f7f\u7528\uff0c\u6574\u5408\u7121\u6cd5\u4f7f\u7528\u3002\n\n\u6309\u4e0b\u300c\u50b3\u9001\u300d\u4ee5\u79fb\u9664\u7531 Home Assistant \u79fb\u9664 Flu Near You\u3002", - "title": "\u79fb\u9664 Flu Near You" - } - } - }, - "title": "Flu Near You \u5df2\u7d93\u7121\u6cd5\u4f7f\u7528" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/flux_led/__init__.py b/homeassistant/components/flux_led/__init__.py index 0284bf90ba0..717b4a685df 100644 --- a/homeassistant/components/flux_led/__init__.py +++ b/homeassistant/components/flux_led/__init__.py @@ -58,7 +58,12 @@ PLATFORMS_BY_TYPE: Final = { Platform.SENSOR, Platform.SWITCH, ], - DeviceType.Switch: [Platform.BUTTON, Platform.SELECT, Platform.SWITCH], + DeviceType.Switch: [ + Platform.BUTTON, + Platform.SELECT, + Platform.SENSOR, + Platform.SWITCH, + ], } DISCOVERY_INTERVAL: Final = timedelta(minutes=15) REQUEST_REFRESH_DELAY: Final = 1.5 diff --git a/homeassistant/components/flux_led/translations/pt.json b/homeassistant/components/flux_led/translations/pt.json index 5e78131c687..c8c04537f40 100644 --- a/homeassistant/components/flux_led/translations/pt.json +++ b/homeassistant/components/flux_led/translations/pt.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" }, - "flow_title": "", + "flow_title": "{model} {id} ({ipaddr})", "step": { "user": { "data": { diff --git a/homeassistant/components/forked_daapd/browse_media.py b/homeassistant/components/forked_daapd/browse_media.py new file mode 100644 index 00000000000..099a042f58a --- /dev/null +++ b/homeassistant/components/forked_daapd/browse_media.py @@ -0,0 +1,333 @@ +"""Browse media for forked-daapd.""" +from __future__ import annotations + +from collections.abc import Sequence +from dataclasses import dataclass +from typing import TYPE_CHECKING, Any, Union, cast +from urllib.parse import quote, unquote + +from homeassistant.components.media_player import BrowseMedia, MediaClass, MediaType +from homeassistant.components.media_player.errors import BrowseError +from homeassistant.helpers.network import is_internal_request + +from .const import CAN_PLAY_TYPE, URI_SCHEMA + +if TYPE_CHECKING: + from . import media_player + +MEDIA_TYPE_DIRECTORY = "directory" + +TOP_LEVEL_LIBRARY = { + "Albums": (MediaClass.ALBUM, MediaType.ALBUM, ""), + "Artists": (MediaClass.ARTIST, MediaType.ARTIST, ""), + "Playlists": (MediaClass.PLAYLIST, MediaType.PLAYLIST, ""), + "Albums by Genre": (MediaClass.GENRE, MediaType.GENRE, MediaType.ALBUM), + "Tracks by Genre": (MediaClass.GENRE, MediaType.GENRE, MediaType.TRACK), + "Artists by Genre": (MediaClass.GENRE, MediaType.GENRE, MediaType.ARTIST), + "Directories": (MediaClass.DIRECTORY, MEDIA_TYPE_DIRECTORY, ""), +} +MEDIA_TYPE_TO_MEDIA_CLASS = { + MediaType.ALBUM: MediaClass.ALBUM, + MediaType.APP: MediaClass.APP, + MediaType.ARTIST: MediaClass.ARTIST, + MediaType.TRACK: MediaClass.TRACK, + MediaType.PLAYLIST: MediaClass.PLAYLIST, + MediaType.GENRE: MediaClass.GENRE, + MEDIA_TYPE_DIRECTORY: MediaClass.DIRECTORY, +} +CAN_EXPAND_TYPE = { + MediaType.ALBUM, + MediaType.ARTIST, + MediaType.PLAYLIST, + MediaType.GENRE, + MEDIA_TYPE_DIRECTORY, +} +# The keys and values in the below dict are identical only because the +# HA constants happen to align with the Owntone constants. +OWNTONE_TYPE_TO_MEDIA_TYPE = { + "track": MediaType.TRACK, + "playlist": MediaType.PLAYLIST, + "artist": MediaType.ARTIST, + "album": MediaType.ALBUM, + "genre": MediaType.GENRE, + MediaType.APP: MediaType.APP, # This is just for passthrough + MEDIA_TYPE_DIRECTORY: MEDIA_TYPE_DIRECTORY, # This is just for passthrough +} +MEDIA_TYPE_TO_OWNTONE_TYPE = {v: k for k, v in OWNTONE_TYPE_TO_MEDIA_TYPE.items()} + +# media_content_id is a uri in the form of SCHEMA:Title:OwnToneURI:Subtype (Subtype only used for Genre) +# OwnToneURI is in format library:type:id (for directories, id is path) +# media_content_type - type of item (mostly used to check if playable or can expand) +# Owntone type may differ from media_content_type when media_content_type is a directory +# Owntone type is used in our own branching, but media_content_type is used for determining playability + + +@dataclass +class MediaContent: + """Class for representing Owntone media content.""" + + title: str + type: str + id_or_path: str + subtype: str + + def __init__(self, media_content_id: str) -> None: + """Create MediaContent from media_content_id.""" + ( + _schema, + self.title, + _library, + self.type, + self.id_or_path, + self.subtype, + ) = media_content_id.split(":") + self.title = unquote(self.title) # Title may have special characters + self.id_or_path = unquote(self.id_or_path) # May have special characters + self.type = OWNTONE_TYPE_TO_MEDIA_TYPE[self.type] + + +def create_owntone_uri(media_type: str, id_or_path: str) -> str: + """Create an Owntone uri.""" + return f"library:{MEDIA_TYPE_TO_OWNTONE_TYPE[media_type]}:{quote(id_or_path)}" + + +def create_media_content_id( + title: str, + owntone_uri: str = "", + media_type: str = "", + id_or_path: str = "", + subtype: str = "", +) -> str: + """Create a media_content_id. + + Either owntone_uri or both type and id_or_path must be specified. + """ + if not owntone_uri: + owntone_uri = create_owntone_uri(media_type, id_or_path) + return f"{URI_SCHEMA}:{quote(title)}:{owntone_uri}:{subtype}" + + +def is_owntone_media_content_id(media_content_id: str) -> bool: + """Return whether this media_content_id is from our integration.""" + return media_content_id[: len(URI_SCHEMA)] == URI_SCHEMA + + +def convert_to_owntone_uri(media_content_id: str) -> str: + """Convert media_content_id to Owntone URI.""" + return ":".join(media_content_id.split(":")[2:-1]) + + +async def get_owntone_content( + master: media_player.ForkedDaapdMaster, + media_content_id: str, +) -> BrowseMedia: + """Create response for the given media_content_id.""" + + media_content = MediaContent(media_content_id) + result: list[dict[str, int | str]] | dict[str, Any] | None = None + if media_content.type == MediaType.APP: + return base_owntone_library() + # Query API for next level + if media_content.type == MEDIA_TYPE_DIRECTORY: + # returns tracks, directories, and playlists + directory_path = media_content.id_or_path + if directory_path: + result = await master.api.get_directory(directory=directory_path) + else: + result = await master.api.get_directory() + if result is None: + raise BrowseError( + f"Media not found for {media_content.type} / {media_content_id}" + ) + # Fill in children with subdirectories + children = [] + assert isinstance(result, dict) + for directory in result["directories"]: + path = directory["path"] + children.append( + BrowseMedia( + title=path, + media_class=MediaClass.DIRECTORY, + media_content_id=create_media_content_id( + title=path, media_type=MEDIA_TYPE_DIRECTORY, id_or_path=path + ), + media_content_type=MEDIA_TYPE_DIRECTORY, + can_play=False, + can_expand=True, + ) + ) + result = result["tracks"]["items"] + result["playlists"]["items"] + return create_browse_media_response( + master, + media_content, + cast(list[dict[str, Union[int, str]]], result), + children, + ) + if media_content.id_or_path == "": # top level search + if media_content.type == MediaType.ALBUM: + result = ( + await master.api.get_albums() + ) # list of albums with name, artist, uri + elif media_content.type == MediaType.ARTIST: + result = await master.api.get_artists() # list of artists with name, uri + elif media_content.type == MediaType.GENRE: + if result := await master.api.get_genres(): # returns list of genre names + for item in result: # pylint: disable=not-an-iterable + # add generated genre uris to list of genre names + item["uri"] = create_owntone_uri( + MediaType.GENRE, cast(str, item["name"]) + ) + elif media_content.type == MediaType.PLAYLIST: + result = ( + await master.api.get_playlists() + ) # list of playlists with name, uri + if result is None: + raise BrowseError( + f"Media not found for {media_content.type} / {media_content_id}" + ) + return create_browse_media_response( + master, + media_content, + cast(list[dict[str, Union[int, str]]], result), + ) + # Not a directory or top level of library + # We should have content type and id + if media_content.type == MediaType.ALBUM: + result = await master.api.get_tracks(album_id=media_content.id_or_path) + elif media_content.type == MediaType.ARTIST: + result = await master.api.get_albums(artist_id=media_content.id_or_path) + elif media_content.type == MediaType.GENRE: + if media_content.subtype in { + MediaType.ALBUM, + MediaType.ARTIST, + MediaType.TRACK, + }: + result = await master.api.get_genre( + media_content.id_or_path, media_type=media_content.subtype + ) + elif media_content.type == MediaType.PLAYLIST: + result = await master.api.get_tracks(playlist_id=media_content.id_or_path) + + if result is None: + raise BrowseError( + f"Media not found for {media_content.type} / {media_content_id}" + ) + + return create_browse_media_response( + master, media_content, cast(list[dict[str, Union[int, str]]], result) + ) + + +def create_browse_media_response( + master: media_player.ForkedDaapdMaster, + media_content: MediaContent, + result: list[dict[str, int | str]], + children: list[BrowseMedia] | None = None, +) -> BrowseMedia: + """Convert the results into a browse media response.""" + internal_request = is_internal_request(master.hass) + if not children: # Directory searches will pass in subdirectories as children + children = [] + for item in result: + if item.get("data_kind") == "spotify" or ( + "path" in item and cast(str, item["path"]).startswith("spotify") + ): # Exclude spotify data from Owntone library + continue + assert isinstance(item["uri"], str) + media_type = OWNTONE_TYPE_TO_MEDIA_TYPE[item["uri"].split(":")[1]] + title = item.get("name") or item.get("title") # only tracks use title + assert isinstance(title, str) + media_content_id = create_media_content_id( + title=f"{media_content.title} / {title}", + owntone_uri=item["uri"], + subtype=media_content.subtype, + ) + if artwork := item.get("artwork_url"): + thumbnail = ( + master.api.full_url(cast(str, artwork)) + if internal_request + else master.get_browse_image_url(media_type, media_content_id) + ) + else: + thumbnail = None + children.append( + BrowseMedia( + title=title, + media_class=MEDIA_TYPE_TO_MEDIA_CLASS[media_type], + media_content_id=media_content_id, + media_content_type=media_type, + can_play=media_type in CAN_PLAY_TYPE, + can_expand=media_type in CAN_EXPAND_TYPE, + thumbnail=thumbnail, + ) + ) + return BrowseMedia( + title=media_content.id_or_path + if media_content.type == MEDIA_TYPE_DIRECTORY + else media_content.title, + media_class=MEDIA_TYPE_TO_MEDIA_CLASS[media_content.type], + media_content_id="", + media_content_type=media_content.type, + can_play=media_content.type in CAN_PLAY_TYPE, + can_expand=media_content.type in CAN_EXPAND_TYPE, + children=children, + ) + + +def base_owntone_library() -> BrowseMedia: + """Return the base of our Owntone library.""" + children = [ + BrowseMedia( + title=name, + media_class=media_class, + media_content_id=create_media_content_id( + title=name, media_type=media_type, subtype=media_subtype + ), + media_content_type=MEDIA_TYPE_DIRECTORY, + can_play=False, + can_expand=True, + ) + for name, (media_class, media_type, media_subtype) in TOP_LEVEL_LIBRARY.items() + ] + return BrowseMedia( + title="Owntone Library", + media_class=MediaClass.APP, + media_content_id=create_media_content_id( + title="Owntone Library", media_type=MediaType.APP + ), + media_content_type=MediaType.APP, + can_play=False, + can_expand=True, + children=children, + thumbnail="https://brands.home-assistant.io/_/forked_daapd/logo.png", + ) + + +def library(other: Sequence[BrowseMedia] | None) -> BrowseMedia: + """Create response to describe contents of library.""" + + top_level_items = [ + BrowseMedia( + title="Owntone Library", + media_class=MediaClass.APP, + media_content_id=create_media_content_id( + title="Owntone Library", media_type=MediaType.APP + ), + media_content_type=MediaType.APP, + can_play=False, + can_expand=True, + thumbnail="https://brands.home-assistant.io/_/forked_daapd/logo.png", + ) + ] + if other: + top_level_items.extend(other) + + return BrowseMedia( + title="Owntone", + media_class=MediaClass.DIRECTORY, + media_content_id="", + media_content_type=MEDIA_TYPE_DIRECTORY, + can_play=False, + can_expand=True, + children=top_level_items, + ) diff --git a/homeassistant/components/forked_daapd/const.py b/homeassistant/components/forked_daapd/const.py index f0d915ce3e5..60e31bc707d 100644 --- a/homeassistant/components/forked_daapd/const.py +++ b/homeassistant/components/forked_daapd/const.py @@ -1,6 +1,7 @@ """Const for forked-daapd.""" -from homeassistant.components.media_player import MediaPlayerEntityFeature +from homeassistant.components.media_player import MediaPlayerEntityFeature, MediaType +CALLBACK_TIMEOUT = 8 # max time between command and callback from forked-daapd server CAN_PLAY_TYPE = { "audio/mp4", "audio/aac", @@ -10,9 +11,15 @@ CAN_PLAY_TYPE = { "audio/x-ms-wma", "audio/aiff", "audio/wav", + MediaType.TRACK, + MediaType.PLAYLIST, + MediaType.ARTIST, + MediaType.ALBUM, + MediaType.GENRE, + MediaType.MUSIC, + MediaType.EPISODE, + "show", # this is a spotify constant } - -CALLBACK_TIMEOUT = 8 # max time between command and callback from forked-daapd server CONF_LIBRESPOT_JAVA_PORT = "librespot_java_port" CONF_MAX_PLAYLISTS = "max_playlists" CONF_TTS_PAUSE_TIME = "tts_pause_time" @@ -83,3 +90,4 @@ SUPPORTED_FEATURES_ZONE = ( | MediaPlayerEntityFeature.TURN_OFF ) TTS_TIMEOUT = 20 # max time to wait between TTS getting sent and starting to play +URI_SCHEMA = "owntone" diff --git a/homeassistant/components/forked_daapd/manifest.json b/homeassistant/components/forked_daapd/manifest.json index 9a0372a193e..14d2132a165 100644 --- a/homeassistant/components/forked_daapd/manifest.json +++ b/homeassistant/components/forked_daapd/manifest.json @@ -4,6 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/forked_daapd", "codeowners": ["@uvjustin"], "requirements": ["pyforked-daapd==0.1.11", "pylibrespot-java==0.1.0"], + "after_dependencies": ["spotify"], "config_flow": true, "zeroconf": ["_daap._tcp.local."], "iot_class": "local_push", diff --git a/homeassistant/components/forked_daapd/media_player.py b/homeassistant/components/forked_daapd/media_player.py index 953461c1019..9da1c1a1168 100644 --- a/homeassistant/components/forked_daapd/media_player.py +++ b/homeassistant/components/forked_daapd/media_player.py @@ -6,26 +6,29 @@ from collections import defaultdict import logging from typing import Any +import async_timeout from pyforked_daapd import ForkedDaapdAPI from pylibrespot_java import LibrespotJavaAPI from homeassistant.components import media_source -from homeassistant.components.media_player import BrowseMedia, MediaPlayerEntity -from homeassistant.components.media_player.browse_media import ( +from homeassistant.components.media_player import ( + ATTR_MEDIA_ANNOUNCE, + ATTR_MEDIA_ENQUEUE, + BrowseMedia, + MediaPlayerEnqueue, + MediaPlayerEntity, + MediaPlayerState, + MediaType, async_process_play_media_url, ) -from homeassistant.components.media_player.const import MEDIA_TYPE_MUSIC -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - CONF_HOST, - CONF_PASSWORD, - CONF_PORT, - STATE_IDLE, - STATE_OFF, - STATE_ON, - STATE_PAUSED, - STATE_PLAYING, +from homeassistant.components.spotify import ( + async_browse_media as spotify_async_browse_media, + is_spotify_media_type, + resolve_spotify_media_type, + spotify_uri_from_media_browser_url, ) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.dispatcher import ( @@ -35,6 +38,12 @@ from homeassistant.helpers.dispatcher import ( from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.dt import utcnow +from .browse_media import ( + convert_to_owntone_uri, + get_owntone_content, + is_owntone_media_content_id, + library, +) from .const import ( CALLBACK_TIMEOUT, CAN_PLAY_TYPE, @@ -171,7 +180,7 @@ class ForkedDaapdZone(MediaPlayerEntity): async def async_toggle(self) -> None: """Toggle the power on the zone.""" - if self.state == STATE_OFF: + if self.state == MediaPlayerState.OFF: await self.async_turn_on() else: await self.async_turn_off() @@ -195,11 +204,11 @@ class ForkedDaapdZone(MediaPlayerEntity): return f"{FD_NAME} output ({self._output['name']})" @property - def state(self) -> str: + def state(self) -> MediaPlayerState: """State of the zone.""" if self._output["selected"]: - return STATE_ON - return STATE_OFF + return MediaPlayerState.ON + return MediaPlayerState.OFF @property def volume_level(self): @@ -241,7 +250,8 @@ class ForkedDaapdMaster(MediaPlayerEntity): self, clientsession, api, ip_address, api_port, api_password, config_entry ): """Initialize the ForkedDaapd Master Device.""" - self._api = api + # Leave the api public so the browse media helpers can use it + self.api = api self._player = STARTUP_DATA[ "player" ] # _player, _outputs, and _queue are loaded straight from api @@ -355,11 +365,8 @@ class ForkedDaapdMaster(MediaPlayerEntity): @callback def _update_queue(self, queue, event): self._queue = queue - if ( - self._tts_requested - and self._queue["count"] == 1 - and self._queue["items"][0]["uri"].find("tts_proxy") != -1 - ): + if self._tts_requested: + # Assume the change was due to the request self._tts_requested = False self._tts_queued = True @@ -422,20 +429,22 @@ class ForkedDaapdMaster(MediaPlayerEntity): async def async_turn_on(self) -> None: """Restore the last on outputs state.""" # restore state - await self._api.set_volume(volume=self._last_volume * 100) + await self.api.set_volume(volume=self._last_volume * 100) if self._last_outputs: - futures = [] + futures: list[asyncio.Task[int]] = [] for output in self._last_outputs: futures.append( - self._api.change_output( - output["id"], - selected=output["selected"], - volume=output["volume"], + asyncio.create_task( + self.api.change_output( + output["id"], + selected=output["selected"], + volume=output["volume"], + ) ) ) await asyncio.wait(futures) else: # enable all outputs - await self._api.set_enabled_outputs( + await self.api.set_enabled_outputs( [output["id"] for output in self._outputs] ) @@ -444,7 +453,7 @@ class ForkedDaapdMaster(MediaPlayerEntity): await self.async_media_pause() self._last_outputs = self._outputs if any(output["selected"] for output in self._outputs): - await self._api.set_enabled_outputs([]) + await self.api.set_enabled_outputs([]) async def async_toggle(self) -> None: """Toggle the power on the device. @@ -452,7 +461,7 @@ class ForkedDaapdMaster(MediaPlayerEntity): Default media player component method counts idle as off. We consider idle to be on but just not playing. """ - if self.state == STATE_OFF: + if self.state == MediaPlayerState.OFF: await self.async_turn_on() else: await self.async_turn_off() @@ -463,16 +472,17 @@ class ForkedDaapdMaster(MediaPlayerEntity): return f"{FD_NAME} server" @property - def state(self): + def state(self) -> MediaPlayerState | None: """State of the player.""" if self._player["state"] == "play": - return STATE_PLAYING + return MediaPlayerState.PLAYING if self._player["state"] == "pause": - return STATE_PAUSED + return MediaPlayerState.PAUSED if not any(output["selected"] for output in self._outputs): - return STATE_OFF + return MediaPlayerState.OFF if self._player["state"] == "stop": # this should catch all remaining cases - return STATE_IDLE + return MediaPlayerState.IDLE + return None @property def volume_level(self): @@ -571,32 +581,32 @@ class ForkedDaapdMaster(MediaPlayerEntity): target_volume = 0 else: target_volume = self._last_volume # restore volume level - await self._api.set_volume(volume=target_volume * 100) + await self.api.set_volume(volume=target_volume * 100) async def async_set_volume_level(self, volume: float) -> None: """Set volume - input range [0,1].""" - await self._api.set_volume(volume=volume * 100) + await self.api.set_volume(volume=volume * 100) async def async_media_play(self) -> None: """Start playback.""" if self._use_pipe_control(): await self._pipe_call(self._use_pipe_control(), "async_media_play") else: - await self._api.start_playback() + await self.api.start_playback() async def async_media_pause(self) -> None: """Pause playback.""" if self._use_pipe_control(): await self._pipe_call(self._use_pipe_control(), "async_media_pause") else: - await self._api.pause_playback() + await self.api.pause_playback() async def async_media_stop(self) -> None: """Stop playback.""" if self._use_pipe_control(): await self._pipe_call(self._use_pipe_control(), "async_media_stop") else: - await self._api.stop_playback() + await self.api.stop_playback() async def async_media_previous_track(self) -> None: """Skip to previous track.""" @@ -605,32 +615,32 @@ class ForkedDaapdMaster(MediaPlayerEntity): self._use_pipe_control(), "async_media_previous_track" ) else: - await self._api.previous_track() + await self.api.previous_track() async def async_media_next_track(self) -> None: """Skip to next track.""" if self._use_pipe_control(): await self._pipe_call(self._use_pipe_control(), "async_media_next_track") else: - await self._api.next_track() + await self.api.next_track() async def async_media_seek(self, position: float) -> None: """Seek to position.""" - await self._api.seek(position_ms=position * 1000) + await self.api.seek(position_ms=position * 1000) async def async_clear_playlist(self) -> None: """Clear playlist.""" - await self._api.clear_queue() + await self.api.clear_queue() async def async_set_shuffle(self, shuffle: bool) -> None: """Enable/disable shuffle mode.""" - await self._api.shuffle(shuffle) + await self.api.shuffle(shuffle) @property def media_image_url(self): """Image url of current playing media.""" if url := self._track_info.get("artwork_url"): - url = self._api.full_url(url) + url = self.api.full_url(url) return url async def _save_and_set_tts_volumes(self): @@ -638,11 +648,11 @@ class ForkedDaapdMaster(MediaPlayerEntity): self._last_volume = self.volume_level self._last_outputs = self._outputs if self._outputs: - await self._api.set_volume(volume=self._tts_volume * 100) + await self.api.set_volume(volume=self._tts_volume * 100) futures = [] for output in self._outputs: futures.append( - self._api.change_output( + self.api.change_output( output["id"], selected=True, volume=self._tts_volume * 100 ) ) @@ -653,90 +663,139 @@ class ForkedDaapdMaster(MediaPlayerEntity): self._pause_requested = True await self.async_media_pause() try: - await asyncio.wait_for( - self._paused_event.wait(), timeout=CALLBACK_TIMEOUT - ) # wait for paused + async with async_timeout.timeout(CALLBACK_TIMEOUT): + await self._paused_event.wait() # wait for paused except asyncio.TimeoutError: self._pause_requested = False self._paused_event.clear() 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 URI.""" + + # Preprocess media_ids if media_source.is_media_source_id(media_id): - media_type = MEDIA_TYPE_MUSIC + media_type = MediaType.MUSIC play_item = await media_source.async_resolve_media( self.hass, media_id, self.entity_id ) media_id = play_item.url + elif is_owntone_media_content_id(media_id): + media_id = convert_to_owntone_uri(media_id) + elif is_spotify_media_type(media_type): + media_type = resolve_spotify_media_type(media_type) + media_id = spotify_uri_from_media_browser_url(media_id) - if media_type == MEDIA_TYPE_MUSIC: + if media_type not in CAN_PLAY_TYPE: + _LOGGER.warning("Media type '%s' not supported", media_type) + return + + if media_type == MediaType.MUSIC: media_id = async_process_play_media_url(self.hass, media_id) + elif media_type not in CAN_PLAY_TYPE: + _LOGGER.warning("Media type '%s' not supported", media_type) + return - saved_state = self.state # save play state - saved_mute = self.is_volume_muted - sleep_future = asyncio.create_task( - asyncio.sleep(self._tts_pause_time) - ) # start timing now, but not exact because of fd buffer + tts latency - await self._pause_and_wait_for_callback() - await self._save_and_set_tts_volumes() - # save position - saved_song_position = self._player["item_progress_ms"] - saved_queue = ( - self._queue if self._queue["count"] > 0 else None - ) # stash queue - if saved_queue: - saved_queue_position = next( - i - for i, item in enumerate(saved_queue["items"]) - if item["id"] == self._player["item_id"] - ) - self._tts_requested = True - await sleep_future - await self._api.add_to_queue(uris=media_id, playback="start", clear=True) - try: - await asyncio.wait_for( - self._tts_playing_event.wait(), timeout=TTS_TIMEOUT - ) - # we have started TTS, now wait for completion - await asyncio.sleep( - self._queue["items"][0]["length_ms"] - / 1000 # player may not have updated yet so grab length from queue - + self._tts_pause_time - ) - except asyncio.TimeoutError: - self._tts_requested = False - _LOGGER.warning("TTS request timed out") - self._tts_playing_event.clear() - # TTS done, return to normal - await self.async_turn_on() # restore outputs and volumes - if saved_mute: # mute if we were muted - await self.async_mute_volume(True) - if self._use_pipe_control(): # resume pipe - await self._api.add_to_queue( - uris=self._sources_uris[self._source], clear=True - ) - if saved_state == STATE_PLAYING: - await self.async_media_play() - else: # restore stashed queue - if saved_queue: - uris = "" - for item in saved_queue["items"]: - uris += item["uri"] + "," - await self._api.add_to_queue( - uris=uris, - playback="start", - playback_from_position=saved_queue_position, - clear=True, - ) - await self._api.seek(position_ms=saved_song_position) - if saved_state == STATE_PAUSED: - await self.async_media_pause() - elif saved_state != STATE_PLAYING: - await self.async_media_stop() - else: - _LOGGER.debug("Media type '%s' not supported", media_type) + if kwargs.get(ATTR_MEDIA_ANNOUNCE): + return await self._async_announce(media_id) + + # if kwargs[ATTR_MEDIA_ENQUEUE] is None, we assume MediaPlayerEnqueue.REPLACE + # if kwargs[ATTR_MEDIA_ENQUEUE] is True, we assume MediaPlayerEnqueue.ADD + # kwargs[ATTR_MEDIA_ENQUEUE] is assumed to never be False + # See https://github.com/home-assistant/architecture/issues/765 + enqueue: bool | MediaPlayerEnqueue = kwargs.get( + ATTR_MEDIA_ENQUEUE, MediaPlayerEnqueue.REPLACE + ) + if enqueue in {True, MediaPlayerEnqueue.ADD, MediaPlayerEnqueue.REPLACE}: + return await self.api.add_to_queue( + uris=media_id, + playback="start", + clear=enqueue == MediaPlayerEnqueue.REPLACE, + ) + + current_position = next( + ( + item["position"] + for item in self._queue["items"] + if item["id"] == self._player["item_id"] + ), + 0, + ) + if enqueue == MediaPlayerEnqueue.NEXT: + return await self.api.add_to_queue( + uris=media_id, + playback="start", + position=current_position + 1, + ) + # enqueue == MediaPlayerEnqueue.PLAY + return await self.api.add_to_queue( + uris=media_id, + playback="start", + position=current_position, + playback_from_position=current_position, + ) + + async def _async_announce(self, media_id: str) -> None: + """Play a URI.""" + saved_state = self.state # save play state + saved_mute = self.is_volume_muted + sleep_future = asyncio.create_task( + asyncio.sleep(self._tts_pause_time) + ) # start timing now, but not exact because of fd buffer + tts latency + await self._pause_and_wait_for_callback() + await self._save_and_set_tts_volumes() + # save position + saved_song_position = self._player["item_progress_ms"] + saved_queue = self._queue if self._queue["count"] > 0 else None # stash queue + if saved_queue: + saved_queue_position = next( + i + for i, item in enumerate(saved_queue["items"]) + if item["id"] == self._player["item_id"] + ) + self._tts_requested = True + await sleep_future + await self.api.add_to_queue(uris=media_id, playback="start", clear=True) + try: + async with async_timeout.timeout(TTS_TIMEOUT): + await self._tts_playing_event.wait() + # we have started TTS, now wait for completion + except asyncio.TimeoutError: + self._tts_requested = False + _LOGGER.warning("TTS request timed out") + await asyncio.sleep( + self._queue["items"][0]["length_ms"] + / 1000 # player may not have updated yet so grab length from queue + + self._tts_pause_time + ) + self._tts_playing_event.clear() + # TTS done, return to normal + await self.async_turn_on() # restore outputs and volumes + if saved_mute: # mute if we were muted + await self.async_mute_volume(True) + if self._use_pipe_control(): # resume pipe + await self.api.add_to_queue( + uris=self._sources_uris[self._source], clear=True + ) + if saved_state == MediaPlayerState.PLAYING: + await self.async_media_play() + return + if not saved_queue: + return + # Restore stashed queue + await self.api.add_to_queue( + uris=",".join(item["uri"] for item in saved_queue["items"]), + playback="start", + playback_from_position=saved_queue_position, + clear=True, + ) + await self.api.seek(position_ms=saved_song_position) + if saved_state == MediaPlayerState.PAUSED: + await self.async_media_pause() + return + if saved_state != MediaPlayerState.PLAYING: + await self.async_media_stop() async def async_select_source(self, source: str) -> None: """Change source. @@ -753,9 +812,9 @@ class ForkedDaapdMaster(MediaPlayerEntity): if not self._use_pipe_control(): # playlist or clear ends up at default self._source = SOURCE_NAME_DEFAULT if self._sources_uris.get(source): # load uris for pipes or playlists - await self._api.add_to_queue(uris=self._sources_uris[source], clear=True) + await self.api.add_to_queue(uris=self._sources_uris[source], clear=True) elif source == SOURCE_NAME_CLEAR: # clear playlist - await self._api.clear_queue() + await self.api.clear_queue() self.async_write_ha_state() def _use_pipe_control(self): @@ -778,11 +837,65 @@ class ForkedDaapdMaster(MediaPlayerEntity): media_content_id: str | None = None, ) -> BrowseMedia: """Implement the websocket media browsing helper.""" - return await media_source.async_browse_media( - self.hass, - media_content_id, - content_filter=lambda bm: bm.media_content_type in CAN_PLAY_TYPE, - ) + if media_content_id is None or media_source.is_media_source_id( + media_content_id + ): + ms_result = await media_source.async_browse_media( + self.hass, + media_content_id, + content_filter=lambda bm: bm.media_content_type in CAN_PLAY_TYPE, + ) + if media_content_type is not None: + return ms_result + other_sources: list[BrowseMedia] = ( + list(ms_result.children) if ms_result.children else [] + ) + if "spotify" in self.hass.config.components and ( + media_content_type is None or is_spotify_media_type(media_content_type) + ): + spotify_result = await spotify_async_browse_media( + self.hass, media_content_type, media_content_id + ) + if media_content_type is not None: + return spotify_result + if spotify_result.children: + other_sources += spotify_result.children + + if media_content_id is None or media_content_type is None: + # This is the base level, so we combine our library with the other sources + return library(other_sources) + + # media_content_type should only be None if media_content_id is None + return await get_owntone_content(self, media_content_id) + + async def async_get_browse_image( + self, + media_content_type: str, + media_content_id: str, + media_image_id: str | None = None, + ) -> tuple[bytes | None, str | None]: + """Fetch image for media browser.""" + + if media_content_type not in { + MediaType.TRACK, + MediaType.ALBUM, + MediaType.ARTIST, + }: + return None, None + owntone_uri = convert_to_owntone_uri(media_content_id) + item_id_str = owntone_uri.rsplit(":", maxsplit=1)[-1] + if media_content_type == MediaType.TRACK: + result = await self.api.get_track(int(item_id_str)) + elif media_content_type == MediaType.ALBUM: + if result := await self.api.get_albums(): + result = next( + (item for item in result if item["id"] == item_id_str), None + ) + elif result := await self.api.get_artists(): + result = next((item for item in result if item["id"] == item_id_str), None) + if url := result.get("artwork_url"): + return await self._async_fetch_image(self.api.full_url(url)) + return None, None class ForkedDaapdUpdater: diff --git a/homeassistant/components/fortios/device_tracker.py b/homeassistant/components/fortios/device_tracker.py index d6992d8c045..d0c08a06441 100644 --- a/homeassistant/components/fortios/device_tracker.py +++ b/homeassistant/components/fortios/device_tracker.py @@ -35,7 +35,7 @@ PLATFORM_SCHEMA = BASE_PLATFORM_SCHEMA.extend( ) -def get_scanner(hass: HomeAssistant, config: ConfigType) -> DeviceScanner | None: +def get_scanner(hass: HomeAssistant, config: ConfigType) -> FortiOSDeviceScanner | None: """Validate the configuration and return a FortiOSDeviceScanner.""" host = config[DOMAIN][CONF_HOST] verify_ssl = config[DOMAIN][CONF_VERIFY_SSL] diff --git a/homeassistant/components/freebox/translations/fr.json b/homeassistant/components/freebox/translations/fr.json index 60289791408..83d6a030d5f 100644 --- a/homeassistant/components/freebox/translations/fr.json +++ b/homeassistant/components/freebox/translations/fr.json @@ -10,7 +10,7 @@ }, "step": { "link": { - "description": "Cliquez sur \u00abSoumettre\u00bb, puis appuyez sur la fl\u00e8che droite du routeur pour enregistrer Freebox avec Home Assistant. \n\n ! [Emplacement du bouton sur le routeur](/static/images/config_freebox.png)", + "description": "Cliquez sur \u00ab\u00a0Valider\u00a0\u00bb puis appuyez sur la fl\u00e8che droite du routeur pour enregistrer la Freebox aupr\u00e8s de Home Assistant. \n\n![Emplacement du bouton sur le routeur](/static/images/config_freebox.png)", "title": "Lien routeur Freebox" }, "user": { diff --git a/homeassistant/components/freedompro/climate.py b/homeassistant/components/freedompro/climate.py index 7076e48c29c..0b5f147c141 100644 --- a/homeassistant/components/freedompro/climate.py +++ b/homeassistant/components/freedompro/climate.py @@ -8,9 +8,9 @@ from typing import Any from aiohttp.client import ClientSession from pyfreedompro import put_state -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( ATTR_HVAC_MODE, + ClimateEntity, ClimateEntityFeature, HVACMode, ) diff --git a/homeassistant/components/fritz/common.py b/homeassistant/components/fritz/common.py index 610dcab6aaa..6364ada9fb2 100644 --- a/homeassistant/components/fritz/common.py +++ b/homeassistant/components/fritz/common.py @@ -20,10 +20,10 @@ from fritzconnection.lib.fritzhosts import FritzHosts from fritzconnection.lib.fritzstatus import FritzStatus from fritzconnection.lib.fritzwlan import DEFAULT_PASSWORD_LENGTH, FritzGuestWLAN -from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER_DOMAIN -from homeassistant.components.device_tracker.const import ( +from homeassistant.components.device_tracker import ( CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME, + DOMAIN as DEVICE_TRACKER_DOMAIN, ) from homeassistant.components.switch import DOMAIN as DEVICE_SWITCH_DOMAIN from homeassistant.config_entries import ConfigEntry @@ -207,38 +207,35 @@ class FritzBoxTools(update_coordinator.DataUpdateCoordinator): self.fritz_hosts = FritzHosts(fc=self.connection) self.fritz_guest_wifi = FritzGuestWLAN(fc=self.connection) self.fritz_status = FritzStatus(fc=self.connection) - info = self.connection.call_action("DeviceInfo:1", "GetInfo") + info = self.fritz_status.get_device_info() _LOGGER.debug( "gathered device info of %s %s", self.host, { - **info, + **vars(info), "NewDeviceLog": "***omitted***", "NewSerialNumber": "***omitted***", }, ) if not self._unique_id: - self._unique_id = info["NewSerialNumber"] + self._unique_id = info.serial_number - self._model = info.get("NewModelName") - self._current_firmware = info.get("NewSoftwareVersion") + self._model = info.model_name + self._current_firmware = info.software_version ( self._update_available, self._latest_firmware, self._release_url, ) = self._update_device_info() - if "Layer3Forwarding1" in self.connection.services: - if connection_type := self.connection.call_action( - "Layer3Forwarding1", "GetDefaultConnectionService" - ).get("NewDefaultConnectionService"): - # Return NewDefaultConnectionService sample: "1.WANPPPConnection.1" - self.device_conn_type = connection_type[2:][:-2] - self.device_is_router = self.connection.call_action( - self.device_conn_type, "GetInfo" - ).get("NewEnable") + + if self.fritz_status.has_wan_support: + self.device_conn_type = ( + self.fritz_status.get_default_connection_service().connection_service + ) + self.device_is_router = self.fritz_status.has_wan_enabled async def _async_update_data(self) -> None: """Update FritzboxTools data.""" @@ -401,11 +398,7 @@ class FritzBoxTools(update_coordinator.DataUpdateCoordinator): wan_access=None, ) - if ( - "Hosts1" not in self.connection.services - or "X_AVM-DE_GetMeshListPath" - not in self.connection.services["Hosts1"].actions - ) or ( + if not self.fritz_status.device_has_mesh_support or ( self._options and self._options.get(CONF_OLD_DISCOVERY, DEFAULT_CONF_OLD_DISCOVERY) ): diff --git a/homeassistant/components/fritz/config_flow.py b/homeassistant/components/fritz/config_flow.py index ea6461cef32..df0277494ec 100644 --- a/homeassistant/components/fritz/config_flow.py +++ b/homeassistant/components/fritz/config_flow.py @@ -13,7 +13,7 @@ from fritzconnection.core.exceptions import FritzConnectionException, FritzSecur import voluptuous as vol from homeassistant.components import ssdp -from homeassistant.components.device_tracker.const import ( +from homeassistant.components.device_tracker import ( CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME, ) diff --git a/homeassistant/components/fritz/manifest.json b/homeassistant/components/fritz/manifest.json index e5828ba76cf..d05668b9917 100644 --- a/homeassistant/components/fritz/manifest.json +++ b/homeassistant/components/fritz/manifest.json @@ -2,7 +2,7 @@ "domain": "fritz", "name": "AVM FRITZ!Box Tools", "documentation": "https://www.home-assistant.io/integrations/fritz", - "requirements": ["fritzconnection==1.8.0", "xmltodict==0.13.0"], + "requirements": ["fritzconnection==1.10.3", "xmltodict==0.13.0"], "dependencies": ["network"], "codeowners": ["@mammuth", "@AaronDavidSchneider", "@chemelli74", "@mib1185"], "config_flow": true, diff --git a/homeassistant/components/fritz/translations/cs.json b/homeassistant/components/fritz/translations/cs.json index 8f114045caf..c4ca8595f52 100644 --- a/homeassistant/components/fritz/translations/cs.json +++ b/homeassistant/components/fritz/translations/cs.json @@ -8,6 +8,7 @@ "error": { "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", "already_in_progress": "Konfigurace ji\u017e prob\u00edh\u00e1", + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed" }, "flow_title": "{name}", @@ -26,7 +27,10 @@ }, "user": { "data": { - "port": "Port" + "host": "Hostitel", + "password": "Heslo", + "port": "Port", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" } } } diff --git a/homeassistant/components/fritz/translations/es.json b/homeassistant/components/fritz/translations/es.json index a5263d4e056..5da1ae4197b 100644 --- a/homeassistant/components/fritz/translations/es.json +++ b/homeassistant/components/fritz/translations/es.json @@ -4,7 +4,7 @@ "already_configured": "El dispositivo ya est\u00e1 configurado", "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", "ignore_ip6_link_local": "La direcci\u00f3n de enlace local IPv6 no es compatible.", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "error": { "already_configured": "El dispositivo ya est\u00e1 configurado", diff --git a/homeassistant/components/fritzbox/climate.py b/homeassistant/components/fritzbox/climate.py index 10fd3cf0177..806f8b2303e 100644 --- a/homeassistant/components/fritzbox/climate.py +++ b/homeassistant/components/fritzbox/climate.py @@ -3,11 +3,11 @@ from __future__ import annotations from typing import Any -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( ATTR_HVAC_MODE, PRESET_COMFORT, PRESET_ECO, + ClimateEntity, ClimateEntityFeature, HVACMode, ) diff --git a/homeassistant/components/fritzbox/coordinator.py b/homeassistant/components/fritzbox/coordinator.py index a94a7483e18..47d2cdca005 100644 --- a/homeassistant/components/fritzbox/coordinator.py +++ b/homeassistant/components/fritzbox/coordinator.py @@ -52,8 +52,9 @@ class FritzboxDataUpdateCoordinator(DataUpdateCoordinator): if ( device.has_powermeter and device.present - and hasattr(device, "voltage") + and isinstance(device.voltage, int) and device.voltage <= 0 + and isinstance(device.power, int) and device.power <= 0 and device.energy <= 0 ): diff --git a/homeassistant/components/fritzbox/manifest.json b/homeassistant/components/fritzbox/manifest.json index 710f7e8f0c4..422db12b68a 100644 --- a/homeassistant/components/fritzbox/manifest.json +++ b/homeassistant/components/fritzbox/manifest.json @@ -2,7 +2,7 @@ "domain": "fritzbox", "name": "AVM FRITZ!SmartHome", "documentation": "https://www.home-assistant.io/integrations/fritzbox", - "requirements": ["pyfritzhome==0.6.5"], + "requirements": ["pyfritzhome==0.6.7"], "ssdp": [ { "st": "urn:schemas-upnp-org:device:fritzbox:1" diff --git a/homeassistant/components/fritzbox/sensor.py b/homeassistant/components/fritzbox/sensor.py index 161dfc196d2..7253fdcf36e 100644 --- a/homeassistant/components/fritzbox/sensor.py +++ b/homeassistant/components/fritzbox/sensor.py @@ -8,7 +8,7 @@ from typing import Final from pyfritzhome.fritzhomedevice import FritzhomeDevice -from homeassistant.components.climate.const import PRESET_COMFORT, PRESET_ECO +from homeassistant.components.climate import PRESET_COMFORT, PRESET_ECO from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, @@ -49,6 +49,52 @@ class FritzSensorEntityDescription( """Description for Fritz!Smarthome sensor entities.""" +def suitable_eco_temperature(device: FritzhomeDevice) -> bool: + """Check suitablity for eco temperature sensor.""" + return device.has_thermostat and device.eco_temperature is not None + + +def suitable_comfort_temperature(device: FritzhomeDevice) -> bool: + """Check suitablity for comfort temperature sensor.""" + return device.has_thermostat and device.comfort_temperature is not None + + +def suitable_nextchange_temperature(device: FritzhomeDevice) -> bool: + """Check suitablity for next scheduled temperature sensor.""" + return device.has_thermostat and device.nextchange_temperature is not None + + +def suitable_nextchange_time(device: FritzhomeDevice) -> bool: + """Check suitablity for next scheduled changed time sensor.""" + return device.has_thermostat and device.nextchange_endperiod is not None + + +def suitable_temperature(device: FritzhomeDevice) -> bool: + """Check suitablity for temperature sensor.""" + return device.has_temperature_sensor and not device.has_thermostat + + +def value_electric_current(device: FritzhomeDevice) -> float: + """Return native value for electric current sensor.""" + if isinstance(device.power, int) and isinstance(device.voltage, int): + return round(device.power / device.voltage, 3) + return 0.0 + + +def value_nextchange_preset(device: FritzhomeDevice) -> str: + """Return native value for next scheduled preset sensor.""" + if device.nextchange_temperature == device.eco_temperature: + return PRESET_ECO + return PRESET_COMFORT + + +def value_scheduled_preset(device: FritzhomeDevice) -> str: + """Return native value for current scheduled preset sensor.""" + if device.nextchange_temperature == device.eco_temperature: + return PRESET_COMFORT + return PRESET_ECO + + SENSOR_TYPES: Final[tuple[FritzSensorEntityDescription, ...]] = ( FritzSensorEntityDescription( key="temperature", @@ -57,9 +103,7 @@ SENSOR_TYPES: Final[tuple[FritzSensorEntityDescription, ...]] = ( device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, - suitable=lambda device: ( - device.has_temperature_sensor and not device.has_thermostat - ), + suitable=suitable_temperature, native_value=lambda device: device.temperature, # type: ignore[no-any-return] ), FritzSensorEntityDescription( @@ -87,7 +131,7 @@ SENSOR_TYPES: Final[tuple[FritzSensorEntityDescription, ...]] = ( device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, suitable=lambda device: device.has_powermeter, # type: ignore[no-any-return] - native_value=lambda device: device.power / 1000 if device.power else 0.0, + native_value=lambda device: round((device.power or 0.0) / 1000, 3), ), FritzSensorEntityDescription( key="voltage", @@ -96,9 +140,7 @@ SENSOR_TYPES: Final[tuple[FritzSensorEntityDescription, ...]] = ( device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, suitable=lambda device: device.has_powermeter, # type: ignore[no-any-return] - native_value=lambda device: device.voltage - if getattr(device, "voltage", None) - else 0.0, + native_value=lambda device: round((device.voltage or 0.0) / 1000, 2), ), FritzSensorEntityDescription( key="electric_current", @@ -107,9 +149,7 @@ SENSOR_TYPES: Final[tuple[FritzSensorEntityDescription, ...]] = ( device_class=SensorDeviceClass.CURRENT, state_class=SensorStateClass.MEASUREMENT, suitable=lambda device: device.has_powermeter, # type: ignore[no-any-return] - native_value=lambda device: device.power / device.voltage / 1000 - if device.power and getattr(device, "voltage", None) - else 0.0, + native_value=value_electric_current, ), FritzSensorEntityDescription( key="total_energy", @@ -118,7 +158,7 @@ SENSOR_TYPES: Final[tuple[FritzSensorEntityDescription, ...]] = ( device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, suitable=lambda device: device.has_powermeter, # type: ignore[no-any-return] - native_value=lambda device: device.energy / 1000 if device.energy else 0.0, + native_value=lambda device: (device.energy or 0.0) / 1000, ), # Thermostat Sensors FritzSensorEntityDescription( @@ -126,8 +166,7 @@ SENSOR_TYPES: Final[tuple[FritzSensorEntityDescription, ...]] = ( name="Comfort Temperature", native_unit_of_measurement=TEMP_CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, - suitable=lambda device: device.has_thermostat - and device.comfort_temperature is not None, + suitable=suitable_comfort_temperature, native_value=lambda device: device.comfort_temperature, # type: ignore[no-any-return] ), FritzSensorEntityDescription( @@ -135,8 +174,7 @@ SENSOR_TYPES: Final[tuple[FritzSensorEntityDescription, ...]] = ( name="Eco Temperature", native_unit_of_measurement=TEMP_CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, - suitable=lambda device: device.has_thermostat - and device.eco_temperature is not None, + suitable=suitable_eco_temperature, native_value=lambda device: device.eco_temperature, # type: ignore[no-any-return] ), FritzSensorEntityDescription( @@ -144,35 +182,27 @@ SENSOR_TYPES: Final[tuple[FritzSensorEntityDescription, ...]] = ( name="Next Scheduled Temperature", native_unit_of_measurement=TEMP_CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, - suitable=lambda device: device.has_thermostat - and device.nextchange_temperature is not None, + suitable=suitable_nextchange_temperature, native_value=lambda device: device.nextchange_temperature, # type: ignore[no-any-return] ), FritzSensorEntityDescription( key="nextchange_time", name="Next Scheduled Change Time", device_class=SensorDeviceClass.TIMESTAMP, - suitable=lambda device: device.has_thermostat - and device.nextchange_endperiod is not None, + suitable=suitable_nextchange_time, native_value=lambda device: utc_from_timestamp(device.nextchange_endperiod), ), FritzSensorEntityDescription( key="nextchange_preset", name="Next Scheduled Preset", - suitable=lambda device: device.has_thermostat - and device.nextchange_temperature is not None, - native_value=lambda device: PRESET_ECO - if device.nextchange_temperature == device.eco_temperature - else PRESET_COMFORT, + suitable=suitable_nextchange_temperature, + native_value=value_nextchange_preset, ), FritzSensorEntityDescription( key="scheduled_preset", name="Current Scheduled Preset", - suitable=lambda device: device.has_thermostat - and device.nextchange_temperature is not None, - native_value=lambda device: PRESET_COMFORT - if device.nextchange_temperature == device.eco_temperature - else PRESET_ECO, + suitable=suitable_nextchange_temperature, + native_value=value_scheduled_preset, ), ) diff --git a/homeassistant/components/fritzbox/translations/es.json b/homeassistant/components/fritzbox/translations/es.json index ed8184d958a..9edd3ec72d6 100644 --- a/homeassistant/components/fritzbox/translations/es.json +++ b/homeassistant/components/fritzbox/translations/es.json @@ -6,7 +6,7 @@ "ignore_ip6_link_local": "La direcci\u00f3n de enlace local IPv6 no es compatible.", "no_devices_found": "No se encontraron dispositivos en la red", "not_supported": "Conectado a AVM FRITZ!Box pero no es capaz de controlar dispositivos Smart Home.", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "error": { "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida" diff --git a/homeassistant/components/fritzbox_callmonitor/config_flow.py b/homeassistant/components/fritzbox_callmonitor/config_flow.py index 5df1e7fc215..982d888d22a 100644 --- a/homeassistant/components/fritzbox_callmonitor/config_flow.py +++ b/homeassistant/components/fritzbox_callmonitor/config_flow.py @@ -5,6 +5,7 @@ from typing import Any, cast from fritzconnection import FritzConnection from fritzconnection.core.exceptions import FritzConnectionException, FritzSecurityError +from fritzconnection.lib.fritzstatus import FritzStatus from requests.exceptions import ConnectionError as RequestsConnectionError import voluptuous as vol @@ -29,10 +30,7 @@ from .const import ( DEFAULT_PORT, DEFAULT_USERNAME, DOMAIN, - FRITZ_ACTION_GET_INFO, FRITZ_ATTR_NAME, - FRITZ_ATTR_SERIAL_NUMBER, - FRITZ_SERVICE_DEVICE_INFO, SERIAL_NUMBER, ) @@ -104,10 +102,9 @@ class FritzBoxCallMonitorConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): fritz_connection = FritzConnection( address=self._host, user=self._username, password=self._password ) - device_info = fritz_connection.call_action( - FRITZ_SERVICE_DEVICE_INFO, FRITZ_ACTION_GET_INFO - ) - self._serial_number = device_info[FRITZ_ATTR_SERIAL_NUMBER] + fritz_status = FritzStatus(fc=fritz_connection) + device_info = fritz_status.get_device_info() + self._serial_number = device_info.serial_number return ConnectResult.SUCCESS except RequestsConnectionError: diff --git a/homeassistant/components/fritzbox_callmonitor/const.py b/homeassistant/components/fritzbox_callmonitor/const.py index 9939a73bc18..ccc5a45e61f 100644 --- a/homeassistant/components/fritzbox_callmonitor/const.py +++ b/homeassistant/components/fritzbox_callmonitor/const.py @@ -18,10 +18,8 @@ ICON_PHONE: Final = "mdi:phone" ATTR_PREFIXES = "prefixes" -FRITZ_ACTION_GET_INFO = "GetInfo" FRITZ_ATTR_NAME = "name" FRITZ_ATTR_SERIAL_NUMBER = "NewSerialNumber" -FRITZ_SERVICE_DEVICE_INFO = "DeviceInfo" UNKNOWN_NAME = "unknown" SERIAL_NUMBER = "serial_number" diff --git a/homeassistant/components/fritzbox_callmonitor/manifest.json b/homeassistant/components/fritzbox_callmonitor/manifest.json index 3c89f68dc11..9dba2143ce1 100644 --- a/homeassistant/components/fritzbox_callmonitor/manifest.json +++ b/homeassistant/components/fritzbox_callmonitor/manifest.json @@ -3,7 +3,7 @@ "name": "AVM FRITZ!Box Call Monitor", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/fritzbox_callmonitor", - "requirements": ["fritzconnection==1.8.0"], + "requirements": ["fritzconnection==1.10.3"], "codeowners": ["@cdce8p"], "iot_class": "local_polling", "loggers": ["fritzconnection"] diff --git a/homeassistant/components/fronius/translations/cs.json b/homeassistant/components/fronius/translations/cs.json index 773ee67a7cf..2b83abc6792 100644 --- a/homeassistant/components/fronius/translations/cs.json +++ b/homeassistant/components/fronius/translations/cs.json @@ -1,11 +1,20 @@ { "config": { "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", "invalid_host": "Neplatn\u00fd hostitel nebo IP adresa" }, "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, - "flow_title": "{device}" + "flow_title": "{device}", + "step": { + "user": { + "data": { + "host": "Hostitel" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 188ecb8ff98..40989f41f19 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -548,11 +548,9 @@ class IndexView(web_urldispatcher.AbstractResource): """Return a dict with additional info useful for introspection.""" return {"panels": list(self.hass.data[DATA_PANELS])} - def freeze(self) -> None: - """Freeze the resource.""" - def raw_match(self, path: str) -> bool: """Perform a raw match against path.""" + return False def get_template(self) -> jinja2.Template: """Get template.""" diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 69fdc6c95a1..e6d5f63272d 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==20220907.2"], + "requirements": ["home-assistant-frontend==20221005.0"], "dependencies": [ "api", "auth", @@ -19,5 +19,6 @@ "websocket_api" ], "codeowners": ["@home-assistant/frontend"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "system" } diff --git a/homeassistant/components/frontier_silicon/media_player.py b/homeassistant/components/frontier_silicon/media_player.py index 87c7c4036f2..309074c1b26 100644 --- a/homeassistant/components/frontier_silicon/media_player.py +++ b/homeassistant/components/frontier_silicon/media_player.py @@ -15,19 +15,10 @@ from homeassistant.components.media_player import ( PLATFORM_SCHEMA, MediaPlayerEntity, MediaPlayerEntityFeature, + MediaPlayerState, + MediaType, ) -from homeassistant.components.media_player.const import MEDIA_TYPE_MUSIC -from homeassistant.const import ( - CONF_HOST, - CONF_NAME, - CONF_PASSWORD, - CONF_PORT, - STATE_IDLE, - STATE_OFF, - STATE_OPENING, - STATE_PAUSED, - STATE_PLAYING, -) +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_PORT from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import DeviceInfo @@ -89,7 +80,7 @@ async def async_setup_platform( class AFSAPIDevice(MediaPlayerEntity): """Representation of a Frontier Silicon device on the network.""" - _attr_media_content_type: str = MEDIA_TYPE_MUSIC + _attr_media_content_type: str = MediaType.MUSIC _attr_supported_features = ( MediaPlayerEntityFeature.PAUSE @@ -132,14 +123,14 @@ class AFSAPIDevice(MediaPlayerEntity): if await afsapi.get_power(): status = await afsapi.get_play_status() self._attr_state = { - PlayState.PLAYING: STATE_PLAYING, - PlayState.PAUSED: STATE_PAUSED, - PlayState.STOPPED: STATE_IDLE, - PlayState.LOADING: STATE_OPENING, - None: STATE_IDLE, + PlayState.PLAYING: MediaPlayerState.PLAYING, + PlayState.PAUSED: MediaPlayerState.PAUSED, + PlayState.STOPPED: MediaPlayerState.IDLE, + PlayState.LOADING: MediaPlayerState.BUFFERING, + None: MediaPlayerState.IDLE, }.get(status) else: - self._attr_state = STATE_OFF + self._attr_state = MediaPlayerState.OFF except FSConnectionError: if self._attr_available: _LOGGER.warning( @@ -186,7 +177,7 @@ class AFSAPIDevice(MediaPlayerEntity): if not self._max_volume: self._max_volume = int(await afsapi.get_volume_steps() or 1) - 1 - if self._attr_state != STATE_OFF: + if self._attr_state != MediaPlayerState.OFF: info_name = await afsapi.get_play_name() info_text = await afsapi.get_play_text() @@ -251,7 +242,7 @@ class AFSAPIDevice(MediaPlayerEntity): async def async_media_play_pause(self) -> None: """Send play/pause command.""" - if self._attr_state == STATE_PLAYING: + if self._attr_state == MediaPlayerState.PLAYING: await self.fs_device.pause() else: await self.fs_device.play() diff --git a/homeassistant/components/fully_kiosk/const.py b/homeassistant/components/fully_kiosk/const.py index 56248544b81..4af7628ed63 100644 --- a/homeassistant/components/fully_kiosk/const.py +++ b/homeassistant/components/fully_kiosk/const.py @@ -5,7 +5,7 @@ from datetime import timedelta import logging from typing import Final -from homeassistant.components.media_player.const import MediaPlayerEntityFeature +from homeassistant.components.media_player import MediaPlayerEntityFeature DOMAIN: Final = "fully_kiosk" diff --git a/homeassistant/components/fully_kiosk/media_player.py b/homeassistant/components/fully_kiosk/media_player.py index 732f88170e1..ae6cf083ed1 100644 --- a/homeassistant/components/fully_kiosk/media_player.py +++ b/homeassistant/components/fully_kiosk/media_player.py @@ -4,13 +4,13 @@ from __future__ import annotations from typing import Any from homeassistant.components import media_source -from homeassistant.components.media_player import MediaPlayerEntity -from homeassistant.components.media_player.browse_media import ( +from homeassistant.components.media_player import ( BrowseMedia, + MediaPlayerEntity, + MediaPlayerState, async_process_play_media_url, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import STATE_IDLE, STATE_PLAYING from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -34,12 +34,12 @@ class FullyMediaPlayer(FullyKioskEntity, MediaPlayerEntity): _attr_supported_features = MEDIA_SUPPORT_FULLYKIOSK _attr_assumed_state = True - _attr_state = STATE_IDLE def __init__(self, coordinator: FullyKioskDataUpdateCoordinator) -> None: """Initialize the media player entity.""" super().__init__(coordinator) self._attr_unique_id = f"{coordinator.data['deviceID']}-mediaplayer" + self._attr_state = MediaPlayerState.IDLE async def async_play_media( self, media_type: str, media_id: str, **kwargs: Any @@ -52,13 +52,13 @@ class FullyMediaPlayer(FullyKioskEntity, MediaPlayerEntity): media_id = async_process_play_media_url(self.hass, play_item.url) await self.coordinator.fully.playSound(media_id, AUDIOMANAGER_STREAM_MUSIC) - self._attr_state = STATE_PLAYING + self._attr_state = MediaPlayerState.PLAYING self.async_write_ha_state() async def async_media_stop(self) -> None: """Stop playing media.""" await self.coordinator.fully.stopSound() - self._attr_state = STATE_IDLE + self._attr_state = MediaPlayerState.IDLE self.async_write_ha_state() async def async_set_volume_level(self, volume: float) -> None: diff --git a/homeassistant/components/fully_kiosk/translations/bg.json b/homeassistant/components/fully_kiosk/translations/bg.json new file mode 100644 index 00000000000..8dbd96c7099 --- /dev/null +++ b/homeassistant/components/fully_kiosk/translations/bg.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u0410\u043a\u0430\u0443\u043d\u0442\u044a\u0442 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435", + "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", + "password": "\u041f\u0430\u0440\u043e\u043b\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fully_kiosk/translations/cs.json b/homeassistant/components/fully_kiosk/translations/cs.json new file mode 100644 index 00000000000..737979c68e8 --- /dev/null +++ b/homeassistant/components/fully_kiosk/translations/cs.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u00da\u010det je ji\u017e nastaven" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "host": "Hostitel", + "password": "Heslo" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fully_kiosk/translations/nl.json b/homeassistant/components/fully_kiosk/translations/nl.json new file mode 100644 index 00000000000..359990b3e69 --- /dev/null +++ b/homeassistant/components/fully_kiosk/translations/nl.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Account is al geconfigureerd" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "invalid_auth": "Ongeldige authenticatie", + "unknown": "Onverwachte fout" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Wachtwoord" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fully_kiosk/translations/sv.json b/homeassistant/components/fully_kiosk/translations/sv.json new file mode 100644 index 00000000000..3aaa8ccb3aa --- /dev/null +++ b/homeassistant/components/fully_kiosk/translations/sv.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Konto har redan konfigurerats" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "data": { + "host": "V\u00e4rd", + "password": "L\u00f6senord" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/garages_amsterdam/translations/cs.json b/homeassistant/components/garages_amsterdam/translations/cs.json index 5073c9248e0..6cb98e7ad68 100644 --- a/homeassistant/components/garages_amsterdam/translations/cs.json +++ b/homeassistant/components/garages_amsterdam/translations/cs.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" } diff --git a/homeassistant/components/generic/manifest.json b/homeassistant/components/generic/manifest.json index 98ca2986a86..8749a45d3de 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.0b4", "pillow==9.2.0"], + "requirements": ["ha-av==10.0.0b5", "pillow==9.2.0"], "documentation": "https://www.home-assistant.io/integrations/generic", "codeowners": ["@davet2001"], "iot_class": "local_push" diff --git a/homeassistant/components/generic/translations/cs.json b/homeassistant/components/generic/translations/cs.json index 73c3e470129..f6756ac66a9 100644 --- a/homeassistant/components/generic/translations/cs.json +++ b/homeassistant/components/generic/translations/cs.json @@ -3,6 +3,9 @@ "abort": { "no_devices_found": "V s\u00edti nebyla nalezena \u017e\u00e1dn\u00e1 za\u0159\u00edzen\u00ed" }, + "error": { + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, "step": { "content_type": { "data": { @@ -19,6 +22,9 @@ } }, "options": { + "error": { + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, "step": { "content_type": { "data": { diff --git a/homeassistant/components/generic_hygrostat/humidifier.py b/homeassistant/components/generic_hygrostat/humidifier.py index 07d2d2a36f0..0f111ca3d87 100644 --- a/homeassistant/components/generic_hygrostat/humidifier.py +++ b/homeassistant/components/generic_hygrostat/humidifier.py @@ -5,16 +5,14 @@ import asyncio import logging from homeassistant.components.humidifier import ( + ATTR_HUMIDITY, + MODE_AWAY, + MODE_NORMAL, PLATFORM_SCHEMA, HumidifierDeviceClass, HumidifierEntity, HumidifierEntityFeature, ) -from homeassistant.components.humidifier.const import ( - ATTR_HUMIDITY, - MODE_AWAY, - MODE_NORMAL, -) from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_MODE, diff --git a/homeassistant/components/generic_thermostat/climate.py b/homeassistant/components/generic_thermostat/climate.py index f095037d7f7..dadfc995e03 100644 --- a/homeassistant/components/generic_thermostat/climate.py +++ b/homeassistant/components/generic_thermostat/climate.py @@ -8,15 +8,16 @@ from typing import Any import voluptuous as vol -from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( ATTR_PRESET_MODE, + PLATFORM_SCHEMA, PRESET_ACTIVITY, PRESET_AWAY, PRESET_COMFORT, PRESET_HOME, PRESET_NONE, PRESET_SLEEP, + ClimateEntity, ClimateEntityFeature, HVACAction, HVACMode, diff --git a/homeassistant/components/geniushub/climate.py b/homeassistant/components/geniushub/climate.py index 429d51c0035..3f8fb0c6805 100644 --- a/homeassistant/components/geniushub/climate.py +++ b/homeassistant/components/geniushub/climate.py @@ -1,10 +1,10 @@ """Support for Genius Hub climate devices.""" from __future__ import annotations -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( PRESET_ACTIVITY, PRESET_BOOST, + ClimateEntity, ClimateEntityFeature, HVACAction, HVACMode, diff --git a/homeassistant/components/geo_location/__init__.py b/homeassistant/components/geo_location/__init__.py index 54542fa8503..af64443ca28 100644 --- a/homeassistant/components/geo_location/__init__.py +++ b/homeassistant/components/geo_location/__init__.py @@ -16,8 +16,6 @@ from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.typing import ConfigType -# mypy: allow-untyped-defs, no-check-untyped-defs - _LOGGER = logging.getLogger(__name__) ATTR_DISTANCE = "distance" @@ -29,10 +27,12 @@ ENTITY_ID_FORMAT = DOMAIN + ".{}" SCAN_INTERVAL = timedelta(seconds=60) +# mypy: disallow-any-generics + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Geolocation component.""" - component = hass.data[DOMAIN] = EntityComponent( + component = hass.data[DOMAIN] = EntityComponent[GeolocationEvent]( _LOGGER, DOMAIN, hass, SCAN_INTERVAL ) await component.async_setup(config) @@ -41,13 +41,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry.""" - component: EntityComponent = hass.data[DOMAIN] + component: EntityComponent[GeolocationEvent] = hass.data[DOMAIN] return await component.async_setup_entry(entry) async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - component: EntityComponent = hass.data[DOMAIN] + component: EntityComponent[GeolocationEvent] = hass.data[DOMAIN] return await component.async_unload_entry(entry) diff --git a/homeassistant/components/geo_location/manifest.json b/homeassistant/components/geo_location/manifest.json index 2e0d7061099..3a0cb7eae91 100644 --- a/homeassistant/components/geo_location/manifest.json +++ b/homeassistant/components/geo_location/manifest.json @@ -3,5 +3,6 @@ "name": "Geolocation", "documentation": "https://www.home-assistant.io/integrations/geo_location", "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "entity" } diff --git a/homeassistant/components/geo_location/trigger.py b/homeassistant/components/geo_location/trigger.py index bc04490e76c..24632e78454 100644 --- a/homeassistant/components/geo_location/trigger.py +++ b/homeassistant/components/geo_location/trigger.py @@ -1,10 +1,20 @@ """Offer geolocation automation rules.""" +from __future__ import annotations + import logging +from typing import Final import voluptuous as vol from homeassistant.const import CONF_EVENT, CONF_PLATFORM, CONF_SOURCE, CONF_ZONE -from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback +from homeassistant.core import ( + CALLBACK_TYPE, + Event, + HassJob, + HomeAssistant, + State, + callback, +) from homeassistant.helpers import condition, config_validation as cv from homeassistant.helpers.config_validation import entity_domain from homeassistant.helpers.event import TrackStates, async_track_state_change_filtered @@ -13,13 +23,11 @@ from homeassistant.helpers.typing import ConfigType from . import DOMAIN -# mypy: allow-untyped-defs, no-check-untyped-defs - _LOGGER = logging.getLogger(__name__) -EVENT_ENTER = "enter" -EVENT_LEAVE = "leave" -DEFAULT_EVENT = EVENT_ENTER +EVENT_ENTER: Final = "enter" +EVENT_LEAVE: Final = "leave" +DEFAULT_EVENT: Final = EVENT_ENTER TRIGGER_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend( { @@ -33,9 +41,9 @@ TRIGGER_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend( ) -def source_match(state, source): +def source_match(state: State | None, source: str) -> bool: """Check if the state matches the provided source.""" - return state and state.attributes.get("source") == source + return state is not None and state.attributes.get("source") == source async def async_attach_trigger( @@ -47,12 +55,12 @@ async def async_attach_trigger( """Listen for state changes based on configuration.""" trigger_data = trigger_info["trigger_data"] source: str = config[CONF_SOURCE].lower() - zone_entity_id = config.get(CONF_ZONE) - trigger_event = config.get(CONF_EVENT) + zone_entity_id: str = config[CONF_ZONE] + trigger_event: str = config[CONF_EVENT] job = HassJob(action) @callback - def state_change_listener(event): + def state_change_listener(event: Event) -> None: """Handle specific state changes.""" # Skip if the event's source does not match the trigger's source. from_state = event.data.get("old_state") diff --git a/homeassistant/components/geocaching/translations/cs.json b/homeassistant/components/geocaching/translations/cs.json new file mode 100644 index 00000000000..5b7d9c2db8e --- /dev/null +++ b/homeassistant/components/geocaching/translations/cs.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "\u00da\u010det je ji\u017e nastaven", + "already_in_progress": "Konfigurace ji\u017e prob\u00edh\u00e1", + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" + }, + "step": { + "reauth_confirm": { + "title": "Znovu ov\u011b\u0159it integraci" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/geocaching/translations/es.json b/homeassistant/components/geocaching/translations/es.json index 357eeba9000..1060c57258d 100644 --- a/homeassistant/components/geocaching/translations/es.json +++ b/homeassistant/components/geocaching/translations/es.json @@ -7,7 +7,7 @@ "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})", "oauth_error": "Se han recibido datos de token no v\u00e1lidos.", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "create_entry": { "default": "Autenticado correctamente" diff --git a/homeassistant/components/github/__init__.py b/homeassistant/components/github/__init__.py index 53b8cd67871..bc2bab49bdb 100644 --- a/homeassistant/components/github/__init__.py +++ b/homeassistant/components/github/__init__.py @@ -38,7 +38,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) await coordinator.async_config_entry_first_refresh() - await coordinator.subscribe() + + if not entry.pref_disable_polling: + await coordinator.subscribe() hass.data[DOMAIN][repository] = coordinator diff --git a/homeassistant/components/github/translations/ja.json b/homeassistant/components/github/translations/ja.json index f7dfc37f64d..614b37f714e 100644 --- a/homeassistant/components/github/translations/ja.json +++ b/homeassistant/components/github/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u30b5\u30fc\u30d3\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", - "could_not_register": "\u7d71\u5408\u3068GitHub\u3068\u306e\u767b\u9332\u304c\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f" + "could_not_register": "GitHub \u3068\u306e\u7d71\u5408\u3092\u767b\u9332\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f" }, "progress": { "wait_for_device": "1. {url} \u3092\u958b\u304f\n2. \u6b21\u306e\u30ad\u30fc\u3092\u8cbc\u308a\u4ed8\u3051\u3066\u3001\u7d71\u5408\u3092\u8a8d\u8a3c\u3057\u307e\u3059\u3002\n```\n{code}\n```\n" diff --git a/homeassistant/components/goalzero/translations/cs.json b/homeassistant/components/goalzero/translations/cs.json index c8d0ab45fd6..9097c3351dd 100644 --- a/homeassistant/components/goalzero/translations/cs.json +++ b/homeassistant/components/goalzero/translations/cs.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u00da\u010det je ji\u017e nastaven", + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, "error": { diff --git a/homeassistant/components/goodwe/number.py b/homeassistant/components/goodwe/number.py index f8d3979879f..b8196b05252 100644 --- a/homeassistant/components/goodwe/number.py +++ b/homeassistant/components/goodwe/number.py @@ -128,7 +128,6 @@ class InverterNumberEntity(NumberEntity): async def async_set_native_value(self, value: float) -> None: """Set new value.""" - if self.entity_description.setter: - await self.entity_description.setter(self._inverter, int(value)) + await self.entity_description.setter(self._inverter, int(value)) self._attr_native_value = value self.async_write_ha_state() diff --git a/homeassistant/components/goodwe/sensor.py b/homeassistant/components/goodwe/sensor.py index 6dcdc6e8cb1..bf01f449724 100644 --- a/homeassistant/components/goodwe/sensor.py +++ b/homeassistant/components/goodwe/sensor.py @@ -3,6 +3,7 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass +from datetime import timedelta from typing import Any, cast from goodwe import Inverter, Sensor, SensorKind @@ -23,19 +24,28 @@ from homeassistant.const import ( POWER_WATT, TEMP_CELSIUS, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity import DeviceInfo, EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.event import async_track_point_in_time from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, ) +import homeassistant.util.dt as dt_util from .const import DOMAIN, KEY_COORDINATOR, KEY_DEVICE_INFO, KEY_INVERTER # Sensor name of battery SoC BATTERY_SOC = "battery_soc" +# Sensors that are reset to 0 at midnight. +# The inverter is only powered by the solar panels and not mains power, so it goes dead when the sun goes down. +# The "_day" sensors are reset to 0 when the inverter wakes up in the morning when the sun comes up and power to the inverter is restored. +# This makes sure daily values are reset at midnight instead of at sunrise. +# When the inverter has a battery connected, HomeAssistant will not reset the values but let the inverter reset them by looking at the unavailable state of the inverter. +DAILY_RESET = ["e_day", "e_load_day"] + _MAIN_SENSORS = ( "ppv", "house_consumption", @@ -167,6 +177,7 @@ class InverterSensor(CoordinatorEntity, SensorEntity): self._attr_device_class = SensorDeviceClass.BATTERY self._sensor = sensor self._previous_value = None + self._stop_reset = None @property def native_value(self): @@ -190,3 +201,32 @@ class InverterSensor(CoordinatorEntity, SensorEntity): return cast(GoodweSensorEntityDescription, self.entity_description).available( self ) + + @callback + def async_reset(self, now): + """Reset the value back to 0 at midnight.""" + if not self.coordinator.last_update_success: + self._previous_value = 0 + self.coordinator.data[self._sensor.id_] = 0 + self.async_write_ha_state() + next_midnight = dt_util.start_of_local_day(dt_util.utcnow() + timedelta(days=1)) + self._stop_reset = async_track_point_in_time( + self.hass, self.async_reset, next_midnight + ) + + async def async_added_to_hass(self): + """Schedule reset task at midnight.""" + if self._sensor.id_ in DAILY_RESET: + next_midnight = dt_util.start_of_local_day( + dt_util.utcnow() + timedelta(days=1) + ) + self._stop_reset = async_track_point_in_time( + self.hass, self.async_reset, next_midnight + ) + await super().async_added_to_hass() + + async def async_will_remove_from_hass(self): + """Remove reset task at midnight.""" + if self._sensor.id_ in DAILY_RESET and self._stop_reset is not None: + self._stop_reset() + await super().async_will_remove_from_hass() diff --git a/homeassistant/components/google/__init__.py b/homeassistant/components/google/__init__.py index 7416a9d7793..33a1216085d 100644 --- a/homeassistant/components/google/__init__.py +++ b/homeassistant/components/google/__init__.py @@ -10,20 +10,12 @@ import aiohttp from gcal_sync.api import GoogleCalendarService from gcal_sync.exceptions import ApiException, AuthException from gcal_sync.model import DateOrDatetime, Event -from oauth2client.file import Storage import voluptuous as vol from voluptuous.error import Error as VoluptuousError import yaml -from homeassistant import config_entries -from homeassistant.components.application_credentials import ( - ClientCredential, - async_import_client_credential, -) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - CONF_CLIENT_ID, - CONF_CLIENT_SECRET, CONF_DEVICE_ID, CONF_ENTITIES, CONF_NAME, @@ -40,15 +32,10 @@ from homeassistant.helpers import config_entry_oauth2_flow from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import generate_entity_id -from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue -from homeassistant.helpers.typing import ConfigType from .api import ApiAuthImpl, get_feature_access from .const import ( - CONF_CALENDAR_ACCESS, - DATA_CONFIG, DATA_SERVICE, - DEVICE_AUTH_IMPL, DOMAIN, EVENT_DESCRIPTION, EVENT_END_DATE, @@ -79,37 +66,15 @@ DEFAULT_CONF_OFFSET = "!!" EVENT_CALENDAR_ID = "calendar_id" -NOTIFICATION_ID = "google_calendar_notification" -NOTIFICATION_TITLE = "Google Calendar Setup" -GROUP_NAME_ALL_CALENDARS = "Google Calendar Sensors" - SERVICE_ADD_EVENT = "add_event" YAML_DEVICES = f"{DOMAIN}_calendars.yaml" -TOKEN_FILE = f".{DOMAIN}.token" - PLATFORMS = [Platform.CALENDAR] -CONFIG_SCHEMA = vol.Schema( - vol.All( - cv.deprecated(DOMAIN), - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_CLIENT_ID): cv.string, - vol.Required(CONF_CLIENT_SECRET): cv.string, - vol.Optional(CONF_TRACK_NEW, default=True): cv.boolean, - vol.Optional(CONF_CALENDAR_ACCESS, default="read_write"): cv.enum( - FeatureAccess - ), - } - ) - }, - ), - extra=vol.ALLOW_EXTRA, -) +CONFIG_SCHEMA = vol.Schema(cv.removed(DOMAIN), extra=vol.ALLOW_EXTRA) + _SINGLE_CALSEARCH_CONFIG = vol.All( cv.deprecated(CONF_MAX_RESULTS), @@ -171,65 +136,6 @@ ADD_EVENT_SERVICE_SCHEMA = vol.All( ) -async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up the Google component.""" - if DOMAIN not in config: - return True - - conf = config.get(DOMAIN, {}) - hass.data[DOMAIN] = {DATA_CONFIG: conf} - - if CONF_CLIENT_ID in conf and CONF_CLIENT_SECRET in conf: - await async_import_client_credential( - hass, - DOMAIN, - ClientCredential( - conf[CONF_CLIENT_ID], - conf[CONF_CLIENT_SECRET], - ), - DEVICE_AUTH_IMPL, - ) - - # Import credentials from the old token file into the new way as - # a ConfigEntry managed by home assistant. - storage = Storage(hass.config.path(TOKEN_FILE)) - creds = await hass.async_add_executor_job(storage.get) - if creds and get_feature_access(hass).scope in creds.scopes: - _LOGGER.debug("Importing configuration entry with credentials") - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={ - "creds": creds, - }, - ) - ) - async_create_issue( - hass, - DOMAIN, - "deprecated_yaml", - breaks_in_ha_version="2022.9.0", # Warning first added in 2022.6.0 - is_fixable=False, - severity=IssueSeverity.WARNING, - translation_key="deprecated_yaml", - ) - if conf.get(CONF_TRACK_NEW) is False: - # The track_new as False would previously result in new entries - # in google_calendars.yaml with track set to False which is - # handled at calendar entity creation time. - async_create_issue( - hass, - DOMAIN, - "removed_track_new_yaml", - breaks_in_ha_version="2022.6.0", - is_fixable=False, - severity=IssueSeverity.WARNING, - translation_key="removed_track_new_yaml", - ) - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Google from a config entry.""" hass.data.setdefault(DOMAIN, {}) diff --git a/homeassistant/components/google/manifest.json b/homeassistant/components/google/manifest.json index d39f2093cf0..39c889786b7 100644 --- a/homeassistant/components/google/manifest.json +++ b/homeassistant/components/google/manifest.json @@ -1,6 +1,6 @@ { "domain": "google", - "name": "Google Calendars", + "name": "Google Calendar", "config_flow": true, "dependencies": ["application_credentials"], "documentation": "https://www.home-assistant.io/integrations/calendar.google/", diff --git a/homeassistant/components/google/strings.json b/homeassistant/components/google/strings.json index 58e5cedd98d..b4c5270e003 100644 --- a/homeassistant/components/google/strings.json +++ b/homeassistant/components/google/strings.json @@ -41,15 +41,5 @@ }, "application_credentials": { "description": "Follow the [instructions]({more_info_url}) for [OAuth consent screen]({oauth_consent_url}) to give Home Assistant access to your Google Calendar. You also need to create Application Credentials linked to your Calendar:\n1. Go to [Credentials]({oauth_creds_url}) and click **Create Credentials**.\n1. From the drop-down list select **OAuth client ID**.\n1. Select **TV and Limited Input devices** for the Application Type.\n\n" - }, - "issues": { - "deprecated_yaml": { - "title": "The Google Calendar YAML configuration is being removed", - "description": "Configuring the Google Calendar in configuration.yaml is being removed in Home Assistant 2022.9.\n\nYour existing OAuth Application Credentials and access settings have been imported into the UI automatically. Remove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." - }, - "removed_track_new_yaml": { - "title": "Google Calendar entity tracking has changed", - "description": "You have disabled entity tracking for Google Calendar in configuration.yaml, which is no longer supported. You must manually change the integration System Options in the UI to disable newly discovered entities going forward. Remove the track_new setting from configuration.yaml and restart Home Assistant to fix this issue." - } } } diff --git a/homeassistant/components/google/translations/bg.json b/homeassistant/components/google/translations/bg.json index cd81f011d5a..2fa49447827 100644 --- a/homeassistant/components/google/translations/bg.json +++ b/homeassistant/components/google/translations/bg.json @@ -18,6 +18,11 @@ } } }, + "issues": { + "deprecated_yaml": { + "title": "YAML \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u043d\u0430 Google Calendar \u0441\u0435 \u043f\u0440\u0435\u043c\u0430\u0445\u0432\u0430" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/google/translations/cs.json b/homeassistant/components/google/translations/cs.json index 0c11a65f69b..f1d2cc4c85a 100644 --- a/homeassistant/components/google/translations/cs.json +++ b/homeassistant/components/google/translations/cs.json @@ -3,6 +3,7 @@ "abort": { "already_configured": "\u00da\u010det je ji\u017e nastaven", "already_in_progress": "Konfigurace ji\u017e prob\u00edh\u00e1", + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", "invalid_access_token": "Neplatn\u00fd p\u0159\u00edstupov\u00fd token", "missing_configuration": "Komponenta nen\u00ed nastavena. Postupujte podle dokumentace.", "oauth_error": "P\u0159ijata neplatn\u00e1 data tokenu.", diff --git a/homeassistant/components/google/translations/es.json b/homeassistant/components/google/translations/es.json index 401a1f37b94..107a320eb30 100644 --- a/homeassistant/components/google/translations/es.json +++ b/homeassistant/components/google/translations/es.json @@ -11,7 +11,7 @@ "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 reautenticaci\u00f3n se realiz\u00f3 correctamente", + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente", "timeout_connect": "Tiempo de espera agotado para establecer la conexi\u00f3n" }, "create_entry": { diff --git a/homeassistant/components/google/translations/fr.json b/homeassistant/components/google/translations/fr.json index 389f769cdb9..b404fbb29be 100644 --- a/homeassistant/components/google/translations/fr.json +++ b/homeassistant/components/google/translations/fr.json @@ -33,6 +33,11 @@ } } }, + "issues": { + "deprecated_yaml": { + "title": "La configuration YAML pour Google\u00a0Agenda sera bient\u00f4t supprim\u00e9e" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/google_assistant/__init__.py b/homeassistant/components/google_assistant/__init__.py index 638ccfd9133..644179858a3 100644 --- a/homeassistant/components/google_assistant/__init__.py +++ b/homeassistant/components/google_assistant/__init__.py @@ -11,7 +11,7 @@ from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.helpers import config_validation as cv, device_registry as dr from homeassistant.helpers.typing import ConfigType -from .const import ( +from .const import ( # noqa: F401 CONF_ALIASES, CONF_CLIENT_EMAIL, CONF_ENTITY_CONFIG, @@ -29,6 +29,7 @@ from .const import ( DEFAULT_EXPOSED_DOMAINS, DOMAIN, SERVICE_REQUEST_SYNC, + SOURCE_CLOUD, ) from .const import EVENT_QUERY_RECEIVED # noqa: F401 from .http import GoogleAssistantView, GoogleConfig diff --git a/homeassistant/components/google_assistant/diagnostics.py b/homeassistant/components/google_assistant/diagnostics.py index 01e17e0bcf8..fd4347ddd2c 100644 --- a/homeassistant/components/google_assistant/diagnostics.py +++ b/homeassistant/components/google_assistant/diagnostics.py @@ -3,8 +3,7 @@ from __future__ import annotations from typing import Any -from homeassistant.components.diagnostics import async_redact_data -from homeassistant.components.diagnostics.const import REDACTED +from homeassistant.components.diagnostics import REDACTED, async_redact_data from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant @@ -12,7 +11,11 @@ from homeassistant.helpers.typing import ConfigType from .const import CONF_SECURE_DEVICES_PIN, CONF_SERVICE_ACCOUNT, DATA_CONFIG, DOMAIN from .http import GoogleConfig -from .smart_home import async_devices_sync_response, create_sync_response +from .smart_home import ( + async_devices_query_response, + async_devices_sync_response, + create_sync_response, +) TO_REDACT = [ "uuid", @@ -33,9 +36,11 @@ async def async_get_config_entry_diagnostics( yaml_config: ConfigType = data[DATA_CONFIG] devices = await async_devices_sync_response(hass, config, REDACTED) sync = create_sync_response(REDACTED, devices) + query = await async_devices_query_response(hass, config, devices) return { "config_entry": async_redact_data(entry.as_dict(), TO_REDACT), "yaml_config": async_redact_data(yaml_config, TO_REDACT), "sync": async_redact_data(sync, TO_REDACT), + "query": async_redact_data(query, TO_REDACT), } diff --git a/homeassistant/components/google_assistant/helpers.py b/homeassistant/components/google_assistant/helpers.py index 0b9b12f2f4c..3351af94648 100644 --- a/homeassistant/components/google_assistant/helpers.py +++ b/homeassistant/components/google_assistant/helpers.py @@ -11,6 +11,7 @@ import pprint from aiohttp.web import json_response from awesomeversion import AwesomeVersion +from yarl import URL from homeassistant.components import webhook from homeassistant.const import ( @@ -610,12 +611,8 @@ class GoogleEntity: device["otherDeviceIds"] = [{"deviceId": self.entity_id}] device["customData"] = { "webhookId": self.config.get_local_webhook_id(agent_user_id), - "httpPort": self.hass.http.server_port, + "httpPort": URL(get_url(self.hass, allow_external=False)).port, "uuid": instance_uuid, - # Below can be removed in HA 2022.9 - "httpSSL": self.hass.config.api.use_ssl, - "baseUrl": get_url(self.hass, prefer_external=True), - "proxyDeviceId": agent_user_id, } # Add trait sync attributes diff --git a/homeassistant/components/google_assistant/logbook.py b/homeassistant/components/google_assistant/logbook.py index 0ed5745004d..ac12ae2cb8c 100644 --- a/homeassistant/components/google_assistant/logbook.py +++ b/homeassistant/components/google_assistant/logbook.py @@ -1,8 +1,5 @@ """Describe logbook events.""" -from homeassistant.components.logbook.const import ( - LOGBOOK_ENTRY_MESSAGE, - LOGBOOK_ENTRY_NAME, -) +from homeassistant.components.logbook import LOGBOOK_ENTRY_MESSAGE, LOGBOOK_ENTRY_NAME from homeassistant.core import callback from .const import DOMAIN, EVENT_COMMAND_RECEIVED, SOURCE_CLOUD diff --git a/homeassistant/components/google_assistant/smart_home.py b/homeassistant/components/google_assistant/smart_home.py index 75a3fd76b9b..c034e5fc6ca 100644 --- a/homeassistant/components/google_assistant/smart_home.py +++ b/homeassistant/components/google_assistant/smart_home.py @@ -130,6 +130,11 @@ async def async_devices_query(hass, data, payload): context=data.context, ) + return await async_devices_query_response(hass, data.config, payload_devices) + + +async def async_devices_query_response(hass, config, payload_devices): + """Generate the device serialization.""" devices = {} for device in payload_devices: devid = device["id"] @@ -139,7 +144,7 @@ async def async_devices_query(hass, data, payload): devices[devid] = {"online": False} continue - entity = GoogleEntity(hass, data.config, state) + entity = GoogleEntity(hass, config, state) try: devices[devid] = entity.query_serialize() except Exception: # pylint: disable=broad-except diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index a05e8ebe4aa..a76b0a7d687 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -9,9 +9,11 @@ from homeassistant.components import ( binary_sensor, button, camera, + climate, cover, fan, group, + humidifier, input_boolean, input_button, input_select, @@ -25,10 +27,8 @@ from homeassistant.components import ( switch, vacuum, ) -from homeassistant.components.climate import const as climate -from homeassistant.components.humidifier import const as humidifier from homeassistant.components.lock import STATE_JAMMED, STATE_UNLOCKING -from homeassistant.components.media_player.const import MEDIA_TYPE_CHANNEL +from homeassistant.components.media_player import MediaType from homeassistant.const import ( ATTR_ASSUMED_STATE, ATTR_BATTERY_LEVEL, @@ -68,11 +68,12 @@ from homeassistant.const import ( ) from homeassistant.core import DOMAIN as HA_DOMAIN from homeassistant.helpers.network import get_url -from homeassistant.util import color as color_util, dt, temperature as temp_util +from homeassistant.util import color as color_util, dt from homeassistant.util.percentage import ( ordered_list_item_to_percentage, percentage_to_ordered_list_item, ) +from homeassistant.util.unit_conversion import TemperatureConverter from .const import ( CHALLENGE_ACK_NEEDED, @@ -843,7 +844,9 @@ class TemperatureControlTrait(_Trait): unit = self.hass.config.units.temperature_unit current_temp = self.state.state if current_temp not in (STATE_UNKNOWN, STATE_UNAVAILABLE): - temp = round(temp_util.convert(float(current_temp), unit, TEMP_CELSIUS), 1) + temp = round( + TemperatureConverter.convert(float(current_temp), unit, TEMP_CELSIUS), 1 + ) response["temperatureSetpointCelsius"] = temp response["temperatureAmbientCelsius"] = temp @@ -948,7 +951,7 @@ class TemperatureSettingTrait(_Trait): current_temp = attrs.get(climate.ATTR_CURRENT_TEMPERATURE) if current_temp is not None: response["thermostatTemperatureAmbient"] = round( - temp_util.convert(current_temp, unit, TEMP_CELSIUS), 1 + TemperatureConverter.convert(current_temp, unit, TEMP_CELSIUS), 1 ) current_humidity = attrs.get(climate.ATTR_CURRENT_HUMIDITY) @@ -958,13 +961,13 @@ class TemperatureSettingTrait(_Trait): if operation in (climate.HVACMode.AUTO, climate.HVACMode.HEAT_COOL): if supported & climate.SUPPORT_TARGET_TEMPERATURE_RANGE: response["thermostatTemperatureSetpointHigh"] = round( - temp_util.convert( + TemperatureConverter.convert( attrs[climate.ATTR_TARGET_TEMP_HIGH], unit, TEMP_CELSIUS ), 1, ) response["thermostatTemperatureSetpointLow"] = round( - temp_util.convert( + TemperatureConverter.convert( attrs[climate.ATTR_TARGET_TEMP_LOW], unit, TEMP_CELSIUS ), 1, @@ -972,14 +975,14 @@ class TemperatureSettingTrait(_Trait): else: if (target_temp := attrs.get(ATTR_TEMPERATURE)) is not None: target_temp = round( - temp_util.convert(target_temp, unit, TEMP_CELSIUS), 1 + TemperatureConverter.convert(target_temp, unit, TEMP_CELSIUS), 1 ) response["thermostatTemperatureSetpointHigh"] = target_temp response["thermostatTemperatureSetpointLow"] = target_temp else: if (target_temp := attrs.get(ATTR_TEMPERATURE)) is not None: response["thermostatTemperatureSetpoint"] = round( - temp_util.convert(target_temp, unit, TEMP_CELSIUS), 1 + TemperatureConverter.convert(target_temp, unit, TEMP_CELSIUS), 1 ) return response @@ -992,7 +995,7 @@ class TemperatureSettingTrait(_Trait): max_temp = self.state.attributes[climate.ATTR_MAX_TEMP] if command == COMMAND_THERMOSTAT_TEMPERATURE_SETPOINT: - temp = temp_util.convert( + temp = TemperatureConverter.convert( params["thermostatTemperatureSetpoint"], TEMP_CELSIUS, unit ) if unit == TEMP_FAHRENHEIT: @@ -1013,7 +1016,7 @@ class TemperatureSettingTrait(_Trait): ) elif command == COMMAND_THERMOSTAT_TEMPERATURE_SET_RANGE: - temp_high = temp_util.convert( + temp_high = TemperatureConverter.convert( params["thermostatTemperatureSetpointHigh"], TEMP_CELSIUS, unit ) if unit == TEMP_FAHRENHEIT: @@ -1028,7 +1031,7 @@ class TemperatureSettingTrait(_Trait): ), ) - temp_low = temp_util.convert( + temp_low = TemperatureConverter.convert( params["thermostatTemperatureSetpointLow"], TEMP_CELSIUS, unit ) if unit == TEMP_FAHRENHEIT: @@ -2345,7 +2348,7 @@ class ChannelTrait(_Trait): { ATTR_ENTITY_ID: self.state.entity_id, media_player.ATTR_MEDIA_CONTENT_ID: channel_number, - media_player.ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_CHANNEL, + media_player.ATTR_MEDIA_CONTENT_TYPE: MediaType.CHANNEL, }, blocking=not self.config.should_report_state, context=data.context, diff --git a/homeassistant/components/google_sheets/__init__.py b/homeassistant/components/google_sheets/__init__.py new file mode 100644 index 00000000000..e211693bf21 --- /dev/null +++ b/homeassistant/components/google_sheets/__init__.py @@ -0,0 +1,122 @@ +"""Support for Google Sheets.""" +from __future__ import annotations + +from datetime import datetime + +import aiohttp +from google.auth.exceptions import RefreshError +from google.oauth2.credentials import Credentials +from gspread import Client +import voluptuous as vol + +from homeassistant.config_entries import ConfigEntry, ConfigEntryState +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN +from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.helpers.config_entry_oauth2_flow import ( + OAuth2Session, + async_get_config_entry_implementation, +) +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.selector import ConfigEntrySelector + +from .const import DATA_CONFIG_ENTRY, DEFAULT_ACCESS, DOMAIN + +DATA = "data" +WORKSHEET = "worksheet" + +SERVICE_APPEND_SHEET = "append_sheet" + +SHEET_SERVICE_SCHEMA = vol.All( + { + vol.Required(DATA_CONFIG_ENTRY): ConfigEntrySelector(), + vol.Optional(WORKSHEET): cv.string, + vol.Required(DATA): dict, + }, +) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Google Sheets from a config entry.""" + implementation = await async_get_config_entry_implementation(hass, entry) + session = OAuth2Session(hass, entry, implementation) + try: + await session.async_ensure_token_valid() + except aiohttp.ClientResponseError as err: + if 400 <= err.status < 500: + raise ConfigEntryAuthFailed( + "OAuth session is not valid, reauth required" + ) from err + raise ConfigEntryNotReady from err + except aiohttp.ClientError as err: + raise ConfigEntryNotReady from err + + if not async_entry_has_scopes(hass, entry): + raise ConfigEntryAuthFailed("Required scopes are not present, reauth required") + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = session + + await async_setup_service(hass) + + return True + + +def async_entry_has_scopes(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Verify that the config entry desired scope is present in the oauth token.""" + return DEFAULT_ACCESS in entry.data.get(CONF_TOKEN, {}).get("scope", "").split(" ") + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + 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 True + + +async def async_setup_service(hass: HomeAssistant) -> None: + """Add the services for Google Sheets.""" + + def _append_to_sheet(call: ServiceCall, entry: ConfigEntry) -> None: + """Run append in the executor.""" + service = Client(Credentials(entry.data[CONF_TOKEN][CONF_ACCESS_TOKEN])) + try: + sheet = service.open_by_key(entry.unique_id) + except RefreshError as ex: + entry.async_start_reauth(hass) + raise ex + worksheet = sheet.worksheet(call.data.get(WORKSHEET, sheet.sheet1.title)) + row_data = {"created": str(datetime.now())} | call.data[DATA] + columns: list[str] = next(iter(worksheet.get_values("A1:ZZ1")), []) + row = [row_data.get(column, "") for column in columns] + for key, value in row_data.items(): + if key not in columns: + columns.append(key) + worksheet.update_cell(1, len(columns), key) + row.append(value) + worksheet.append_row(row) + + async def append_to_sheet(call: ServiceCall) -> None: + """Append new line of data to a Google Sheets document.""" + entry: ConfigEntry | None = hass.config_entries.async_get_entry( + call.data[DATA_CONFIG_ENTRY] + ) + if not entry: + raise ValueError(f"Invalid config entry: {call.data[DATA_CONFIG_ENTRY]}") + if not (session := hass.data[DOMAIN].get(entry.entry_id)): + raise ValueError(f"Config entry not loaded: {call.data[DATA_CONFIG_ENTRY]}") + await session.async_ensure_token_valid() + await hass.async_add_executor_job(_append_to_sheet, call, entry) + + hass.services.async_register( + DOMAIN, + SERVICE_APPEND_SHEET, + append_to_sheet, + schema=SHEET_SERVICE_SCHEMA, + ) diff --git a/homeassistant/components/google_sheets/application_credentials.py b/homeassistant/components/google_sheets/application_credentials.py new file mode 100644 index 00000000000..c54356b659e --- /dev/null +++ b/homeassistant/components/google_sheets/application_credentials.py @@ -0,0 +1,27 @@ +"""application_credentials platform for Google Sheets.""" + +import oauth2client + +from homeassistant.components.application_credentials import AuthorizationServer +from homeassistant.core import HomeAssistant + +AUTHORIZATION_SERVER = AuthorizationServer( + oauth2client.GOOGLE_AUTH_URI, oauth2client.GOOGLE_TOKEN_URI +) + + +async def async_get_authorization_server(hass: HomeAssistant) -> AuthorizationServer: + """Return authorization server.""" + return AuthorizationServer( + oauth2client.GOOGLE_AUTH_URI, + oauth2client.GOOGLE_TOKEN_URI, + ) + + +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_sheets/", + "oauth_creds_url": "https://console.cloud.google.com/apis/credentials", + } diff --git a/homeassistant/components/google_sheets/config_flow.py b/homeassistant/components/google_sheets/config_flow.py new file mode 100644 index 00000000000..3805ee9d38b --- /dev/null +++ b/homeassistant/components/google_sheets/config_flow.py @@ -0,0 +1,95 @@ +"""Config flow for Google Sheets integration.""" +from __future__ import annotations + +from collections.abc import Mapping +import logging +from typing import Any + +from google.oauth2.credentials import Credentials +from gspread import Client, GSpreadException + +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, DEFAULT_NAME, DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +class OAuth2FlowHandler( + config_entry_oauth2_flow.AbstractOAuth2FlowHandler, domain=DOMAIN +): + """Config flow to handle Google Sheets 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": 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.""" + service = Client(Credentials(data[CONF_TOKEN][CONF_ACCESS_TOKEN])) + + if self.reauth_entry: + _LOGGER.debug("service.open_by_key") + try: + await self.hass.async_add_executor_job( + service.open_by_key, + self.reauth_entry.unique_id, + ) + except GSpreadException as err: + _LOGGER.error( + "Could not find spreadsheet '%s': %s", + self.reauth_entry.unique_id, + str(err), + ) + return self.async_abort(reason="open_spreadsheet_failure") + + 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") + + try: + doc = await self.hass.async_add_executor_job( + service.create, "Home Assistant" + ) + except GSpreadException as err: + _LOGGER.error("Error creating spreadsheet: %s", str(err)) + return self.async_abort(reason="create_spreadsheet_failure") + + await self.async_set_unique_id(doc.id) + self._abort_if_unique_id_configured() + return self.async_create_entry( + title=DEFAULT_NAME, data=data, description_placeholders={"url": doc.url} + ) diff --git a/homeassistant/components/google_sheets/const.py b/homeassistant/components/google_sheets/const.py new file mode 100644 index 00000000000..f8f065972f9 --- /dev/null +++ b/homeassistant/components/google_sheets/const.py @@ -0,0 +1,10 @@ +"""Constants for Google Sheets integration.""" +from __future__ import annotations + +from typing import Final + +DOMAIN = "google_sheets" + +DATA_CONFIG_ENTRY: Final = "config_entry" +DEFAULT_NAME = "Google Sheets" +DEFAULT_ACCESS = "https://www.googleapis.com/auth/drive.file" diff --git a/homeassistant/components/google_sheets/manifest.json b/homeassistant/components/google_sheets/manifest.json new file mode 100644 index 00000000000..c8d86210b42 --- /dev/null +++ b/homeassistant/components/google_sheets/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "google_sheets", + "name": "Google Sheets", + "config_flow": true, + "dependencies": ["application_credentials"], + "documentation": "https://www.home-assistant.io/integrations/google_sheets/", + "requirements": ["gspread==5.5.0"], + "codeowners": ["@tkdrob"], + "iot_class": "cloud_polling" +} diff --git a/homeassistant/components/google_sheets/services.yaml b/homeassistant/components/google_sheets/services.yaml new file mode 100644 index 00000000000..7524ba50fb5 --- /dev/null +++ b/homeassistant/components/google_sheets/services.yaml @@ -0,0 +1,24 @@ +append_sheet: + name: Append to Sheet + description: Append data to a worksheet in Google Sheets. + fields: + config_entry: + name: Sheet + description: The sheet to add data to + required: true + selector: + config_entry: + integration: google_sheets + worksheet: + name: Worksheet + description: Name of the worksheet. Defaults to the first one in the document. + example: "Sheet1" + selector: + text: + data: + name: Data + description: Data to be appended to the worksheet. This puts the values on a new row underneath the matching column (key). Any new key is placed on the top of a new column. + required: true + example: '{"hello": world, "cool": True, "count": 5}' + selector: + object: diff --git a/homeassistant/components/google_sheets/strings.json b/homeassistant/components/google_sheets/strings.json new file mode 100644 index 00000000000..33230038cdf --- /dev/null +++ b/homeassistant/components/google_sheets/strings.json @@ -0,0 +1,39 @@ +{ + "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 Sheets integration needs to re-authenticate your account" + }, + "auth": { + "title": "Link Google Account" + }, + "reauth_confirm": { + "title": "[%key:common::config_flow::title::reauth%]", + "description": "The Google Sheets integration needs to re-authenticate your 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%]", + "create_spreadsheet_failure": "Error while creating spreadsheet, see error log for details", + "open_spreadsheet_failure": "Error while opening spreadsheet, see error log for details" + }, + "create_entry": { + "default": "Successfully authenticated and spreadsheet created at: {url}" + } + }, + "application_credentials": { + "description": "Follow the [instructions]({more_info_url}) for [OAuth consent screen]({oauth_consent_url}) to give Home Assistant access to your Google Sheets. 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_sheets/translations/bg.json b/homeassistant/components/google_sheets/translations/bg.json new file mode 100644 index 00000000000..80ba164940b --- /dev/null +++ b/homeassistant/components/google_sheets/translations/bg.json @@ -0,0 +1,26 @@ +{ + "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", + "create_spreadsheet_failure": "\u0413\u0440\u0435\u0448\u043a\u0430 \u043f\u0440\u0438 \u0441\u044a\u0437\u0434\u0430\u0432\u0430\u043d\u0435 \u043d\u0430 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u0430 \u0442\u0430\u0431\u043b\u0438\u0446\u0430, \u0432\u0438\u0436\u0442\u0435 \u0436\u0443\u0440\u043d\u0430\u043b\u0430 \u0437\u0430 \u0433\u0440\u0435\u0448\u043a\u0438 \u0437\u0430 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0441\u0442\u0438", + "missing_configuration": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u044a\u0442 \u043d\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d. \u041c\u043e\u043b\u044f, \u0441\u043b\u0435\u0434\u0432\u0430\u0439\u0442\u0435 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044f\u0442\u0430.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e", + "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\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0438 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u0430\u0442\u0430 \u0442\u0430\u0431\u043b\u0438\u0446\u0430 \u0435 \u0441\u044a\u0437\u0434\u0430\u0434\u0435\u043d\u0430 \u043d\u0430 \u0430\u0434\u0440\u0435\u0441: {url}" + }, + "step": { + "auth": { + "title": "\u0421\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u043d\u0430 \u0430\u043a\u0430\u0443\u043d\u0442 \u0432 Google" + }, + "pick_implementation": { + "title": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u043c\u0435\u0442\u043e\u0434 \u0437\u0430 \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435" + }, + "reauth_confirm": { + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u043d\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google_sheets/translations/ca.json b/homeassistant/components/google_sheets/translations/ca.json new file mode 100644 index 00000000000..35d26781c3a --- /dev/null +++ b/homeassistant/components/google_sheets/translations/ca.json @@ -0,0 +1,34 @@ +{ + "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 teus fulls de c\u00e0lcul 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", + "create_spreadsheet_failure": "Error en crear el full de c\u00e0lcul, consulta el registre d'errors per m\u00e9s informaci\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.", + "open_spreadsheet_failure": "Error en obrir el full de c\u00e0lcul, consulta el registre d'errors per m\u00e9s informaci\u00f3", + "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": "S'ha autenticat correctament i s'ha creat un full de c\u00e0lcul a: {url}" + }, + "step": { + "auth": { + "title": "Vinculaci\u00f3 amb compte de Google" + }, + "pick_implementation": { + "title": "Selecciona el m\u00e8tode d'autenticaci\u00f3" + }, + "reauth_confirm": { + "title": "Reautenticaci\u00f3 de la integraci\u00f3" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google_sheets/translations/de.json b/homeassistant/components/google_sheets/translations/de.json new file mode 100644 index 00000000000..f203b3e1133 --- /dev/null +++ b/homeassistant/components/google_sheets/translations/de.json @@ -0,0 +1,35 @@ +{ + "application_credentials": { + "description": "Folge den [Anweisungen]({more_info_url}) f\u00fcr den [OAuth-Zustimmungsbildschirm]({oauth_consent_url}), um dem Home Assistant Zugriff auf deine Google Sheets zu geben. Du musst auch Anwendungsnachweise erstellen, die mit deinem Konto verkn\u00fcpft sind:\n1. Gehe zu [Anmeldeinformationen]({oauth_creds_url}) und klicke auf **Anmeldeinformationen erstellen**.\n1. W\u00e4hle aus der Dropdown-Liste **OAuth-Client-ID**.\n1. W\u00e4hle **Webanwendung** f\u00fcr den Anwendungstyp.\n\n" + }, + "config": { + "abort": { + "already_configured": "Konto wurde bereits konfiguriert", + "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", + "cannot_connect": "Verbindung fehlgeschlagen", + "create_spreadsheet_failure": "Fehler beim Erstellen der Tabelle, siehe Fehlerprotokoll f\u00fcr Details", + "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.", + "open_spreadsheet_failure": "Fehler beim \u00d6ffnen der Tabelle, siehe Fehlerprotokoll f\u00fcr Details", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich", + "timeout_connect": "Zeit\u00fcberschreitung beim Verbindungsaufbau", + "unknown": "Unerwarteter Fehler" + }, + "create_entry": { + "default": "Erfolgreich authentifiziert und Tabelle erstellt unter: {url}" + }, + "step": { + "auth": { + "title": "Google-Konto verkn\u00fcpfen" + }, + "pick_implementation": { + "title": "W\u00e4hle die Authentifizierungsmethode" + }, + "reauth_confirm": { + "description": "Die Google Sheets-Integration muss dein Konto neu authentifizieren", + "title": "Integration erneut authentifizieren" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google_sheets/translations/el.json b/homeassistant/components/google_sheets/translations/el.json new file mode 100644 index 00000000000..e7527dcb7d3 --- /dev/null +++ b/homeassistant/components/google_sheets/translations/el.json @@ -0,0 +1,31 @@ +{ + "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\u03bd\u03b1\u03af\u03bd\u03b5\u03c3\u03b7\u03c2 OAuth]({oauth_consent_url}) \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03ce\u03c3\u03b5\u03c4\u03b5 \u03c3\u03c4\u03bf\u03bd \u0392\u03bf\u03b7\u03b8\u03cc Home \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7 \u03c3\u03c4\u03b1 \u03a6\u03cd\u03bb\u03bb\u03b1 Google \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 \u03b4\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03ae\u03c1\u03b9\u03b1 \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae\u03c2 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b4\u03b5\u03bc\u03ad\u03bd\u03b1 \u03bc\u03b5 \u03c4\u03bf\u03bd \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc \u03c3\u03b1\u03c2:\n 1. \u039c\u03b5\u03c4\u03b1\u03b2\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03b1 [\u0394\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03ae\u03c1\u03b9\u03b1] ({oauth_creds_url}) \u03ba\u03b1\u03b9 \u03ba\u03ac\u03bd\u03c4\u03b5 \u03ba\u03bb\u03b9\u03ba \u03c3\u03c4\u03bf **\u0394\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03b4\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03b7\u03c1\u03af\u03c9\u03bd**.\n 1. \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 **OAuth \u0391\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03c0\u03b5\u03bb\u03ac\u03c4\u03b7**.\n 1. \u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 **\u0395\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae \u0399\u03c3\u03c4\u03bf\u03cd** \u03b3\u03b9\u03b1 \u03c4\u03bf\u03bd \u03a4\u03cd\u03c0\u03bf \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", + "create_spreadsheet_failure": "\u03a3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03b9\u03ba\u03bf\u03cd \u03c6\u03cd\u03bb\u03bb\u03bf\u03c5, \u03b4\u03b5\u03af\u03c4\u03b5 \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf \u03ba\u03b1\u03c4\u03b1\u03b3\u03c1\u03b1\u03c6\u03ae\u03c2 \u03c3\u03c6\u03b1\u03bb\u03bc\u03ac\u03c4\u03c9\u03bd \u03b3\u03b9\u03b1 \u03bb\u03b5\u03c0\u03c4\u03bf\u03bc\u03ad\u03c1\u03b5\u03b9\u03b5\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.", + "open_spreadsheet_failure": "\u03a3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03ba\u03b1\u03c4\u03ac \u03c4\u03bf \u03ac\u03bd\u03bf\u03b9\u03b3\u03bc\u03b1 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03b9\u03ba\u03bf\u03cd \u03c6\u03cd\u03bb\u03bb\u03bf\u03c5, \u03b4\u03b5\u03af\u03c4\u03b5 \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf \u03ba\u03b1\u03c4\u03b1\u03b3\u03c1\u03b1\u03c6\u03ae\u03c2 \u03c3\u03c6\u03b1\u03bb\u03bc\u03ac\u03c4\u03c9\u03bd \u03b3\u03b9\u03b1 \u03bb\u03b5\u03c0\u03c4\u03bf\u03bc\u03ad\u03c1\u03b5\u03b9\u03b5\u03c2", + "reauth_successful": "\u039f \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2", + "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 \u03ba\u03b1\u03b9 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03c6\u03cd\u03bb\u03bb\u03bf\u03c5 \u03b5\u03c1\u03b3\u03b1\u03c3\u03af\u03b1\u03c2 \u03c3\u03c4\u03bf: {url}" + }, + "step": { + "auth": { + "title": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03bb\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" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google_sheets/translations/en.json b/homeassistant/components/google_sheets/translations/en.json new file mode 100644 index 00000000000..fff3cb88d68 --- /dev/null +++ b/homeassistant/components/google_sheets/translations/en.json @@ -0,0 +1,35 @@ +{ + "application_credentials": { + "description": "Follow the [instructions]({more_info_url}) for [OAuth consent screen]({oauth_consent_url}) to give Home Assistant access to your Google Sheets. 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", + "create_spreadsheet_failure": "Error while creating spreadsheet, see error log for details", + "invalid_access_token": "Invalid access token", + "missing_configuration": "The component is not configured. Please follow the documentation.", + "oauth_error": "Received invalid token data.", + "open_spreadsheet_failure": "Error while opening spreadsheet, see error log for details", + "reauth_successful": "Re-authentication was successful", + "timeout_connect": "Timeout establishing connection", + "unknown": "Unexpected error" + }, + "create_entry": { + "default": "Successfully authenticated and spreadsheet created at: {url}" + }, + "step": { + "auth": { + "title": "Link Google Account" + }, + "pick_implementation": { + "title": "Pick Authentication Method" + }, + "reauth_confirm": { + "description": "The Google Sheets integration needs to re-authenticate your account", + "title": "Reauthenticate Integration" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google_sheets/translations/es.json b/homeassistant/components/google_sheets/translations/es.json new file mode 100644 index 00000000000..5cf0a4ecff4 --- /dev/null +++ b/homeassistant/components/google_sheets/translations/es.json @@ -0,0 +1,35 @@ +{ + "application_credentials": { + "description": "Sigue las [instrucciones]({more_info_url}) para la [pantalla de consentimiento de OAuth]({oauth_consent_url}) para otorgarle a Home Assistant acceso a tus hojas de c\u00e1lculo de Google. Tambi\u00e9n tienes que 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", + "create_spreadsheet_failure": "Error al crear la hoja de c\u00e1lculo, consulta el registro de errores para obtener m\u00e1s detalles.", + "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.", + "open_spreadsheet_failure": "Error al abrir la hoja de c\u00e1lculo, consulta el registro de errores para obtener m\u00e1s detalles", + "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 con \u00e9xito y hoja de c\u00e1lculo creada en: {url}" + }, + "step": { + "auth": { + "title": "Vincular cuenta de Google" + }, + "pick_implementation": { + "title": "Selecciona el m\u00e9todo de autenticaci\u00f3n" + }, + "reauth_confirm": { + "description": "La integraci\u00f3n de Hojas de c\u00e1lculo de Google 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_sheets/translations/et.json b/homeassistant/components/google_sheets/translations/et.json new file mode 100644 index 00000000000..e1e88192389 --- /dev/null +++ b/homeassistant/components/google_sheets/translations/et.json @@ -0,0 +1,31 @@ +{ + "application_credentials": { + "description": "J\u00e4rgi [instructions]({more_info_url}) v\u00e4ljal [OAuth consent screen]({oauth_consent_url}), et anda koduabilisele juurdep\u00e4\u00e4s oma Google'i arvutustabelitele. Samuti pead looma oma kontoga lingitud rakenduse identimisteabe.\n1. Mine aadressile [Credentials]({oauth_creds_url}) ja kl\u00f5psa **Create Credentials**.\n1. Vali ripploendist **OAuthi kliendi ID**.\n1. Vali rakenduse t\u00fc\u00fcbiks **Veebirakendus**.\n\n" + }, + "config": { + "abort": { + "already_configured": "Konto on juba h\u00e4\u00e4lestatud", + "already_in_progress": "Seadistamine on juba k\u00e4imas", + "cannot_connect": "\u00dchendamine nurjus", + "create_spreadsheet_failure": "Viga arvutustabeli loomisel, vt vigade logi \u00fcksikasjadeks", + "invalid_access_token": "Vigane juurdep\u00e4\u00e4suluba", + "missing_configuration": "See osis on seadistamata. Vaata teavet.", + "oauth_error": "Saadi vigane luba", + "open_spreadsheet_failure": "Viga arvutustabelite avamisel, vt vigade logi \u00fcksikasjade kohta", + "reauth_successful": "Taastuvastamine \u00f5nnestus", + "timeout_connect": "\u00dchenduse loomise ajal\u00f5pp", + "unknown": "Ootamatu t\u00f5rge" + }, + "create_entry": { + "default": "Autentimine \u00f5nnestus ja arvutustabel loodud aadressil: {url}" + }, + "step": { + "auth": { + "title": "Google'i konto linkimine" + }, + "pick_implementation": { + "title": "Vali tuvastusmeetod" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google_sheets/translations/fr.json b/homeassistant/components/google_sheets/translations/fr.json new file mode 100644 index 00000000000..b286d61d377 --- /dev/null +++ b/homeassistant/components/google_sheets/translations/fr.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9", + "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours", + "cannot_connect": "\u00c9chec de connexion", + "create_spreadsheet_failure": "Erreur lors de la cr\u00e9ation de la feuille de calcul, consultez le journal des erreurs pour plus de d\u00e9tails", + "invalid_access_token": "Jeton d'acc\u00e8s non valide", + "missing_configuration": "Le composant n'est pas configur\u00e9. Veuillez suivre la documentation.", + "oauth_error": "Des donn\u00e9es de jeton non valides ont \u00e9t\u00e9 re\u00e7ues.", + "open_spreadsheet_failure": "Erreur lors de l'ouverture de la feuille de calcul, consultez le journal des erreurs pour plus de d\u00e9tails", + "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi", + "timeout_connect": "D\u00e9lai d'attente pour \u00e9tablir la connexion expir\u00e9", + "unknown": "Erreur inattendue" + }, + "create_entry": { + "default": "Authentification r\u00e9ussie\u00a0; feuille de calcul cr\u00e9\u00e9e \u00e0 l'adresse\u00a0: {url}" + }, + "step": { + "auth": { + "title": "Associer un compte Google" + }, + "pick_implementation": { + "title": "S\u00e9lectionner une m\u00e9thode d'authentification" + }, + "reauth_confirm": { + "description": "L'int\u00e9gration Google Sheets doit r\u00e9-authentifier votre compte", + "title": "R\u00e9-authentifier l'int\u00e9gration" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google_sheets/translations/he.json b/homeassistant/components/google_sheets/translations/he.json new file mode 100644 index 00000000000..412a09eb52a --- /dev/null +++ b/homeassistant/components/google_sheets/translations/he.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_access_token": "\u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d2\u05d9\u05e9\u05d4 \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "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.", + "oauth_error": "\u05d4\u05ea\u05e7\u05d1\u05dc\u05d5 \u05e0\u05ea\u05d5\u05e0\u05d9 \u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9\u05d9\u05dd.", + "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7", + "timeout_connect": "\u05e4\u05e1\u05e7 \u05d6\u05de\u05df \u05dc\u05d9\u05e6\u05d9\u05e8\u05ea \u05d7\u05d9\u05d1\u05d5\u05e8", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "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/google_sheets/translations/hu.json b/homeassistant/components/google_sheets/translations/hu.json new file mode 100644 index 00000000000..c9d044b57b6 --- /dev/null +++ b/homeassistant/components/google_sheets/translations/hu.json @@ -0,0 +1,35 @@ +{ + "application_credentials": { + "description": "K\u00f6vesse az [utas\u00edt\u00e1sokat]({more_info_url}) az [OAuth hozz\u00e1j\u00e1rul\u00e1si k\u00e9perny\u0151]({oauth_consent_url}) eset\u00e9ben, hogy a Home Assistant hozz\u00e1f\u00e9rhessen a Google Sheets adatlapjaihoz. 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", + "create_spreadsheet_failure": "Hiba a t\u00e1bl\u00e1zat l\u00e9trehoz\u00e1sa k\u00f6zben, a r\u00e9szletek\u00e9rt tekintse meg a hibanapl\u00f3t", + "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.", + "open_spreadsheet_failure": "Hiba a t\u00e1bl\u00e1zat megnyit\u00e1sakor, a r\u00e9szletek\u00e9rt tekintse meg a hibanapl\u00f3t", + "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": "Sikeresen hiteles\u00edtett \u00e9s l\u00e9trehozott t\u00e1bl\u00e1zat a k\u00f6vetkez\u0151 helyen: {url}" + }, + "step": { + "auth": { + "title": "Google-fi\u00f3k \u00f6sszekapcsol\u00e1sa" + }, + "pick_implementation": { + "title": "V\u00e1lasszon egy hiteles\u00edt\u00e9si m\u00f3dszert" + }, + "reauth_confirm": { + "description": "A Google T\u00e1bl\u00e1zatok-integr\u00e1ci\u00f3nak \u00fajra kell hiteles\u00edtenie a fi\u00f3kj\u00e1t", + "title": "Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google_sheets/translations/id.json b/homeassistant/components/google_sheets/translations/id.json new file mode 100644 index 00000000000..474fa65b005 --- /dev/null +++ b/homeassistant/components/google_sheets/translations/id.json @@ -0,0 +1,31 @@ +{ + "application_credentials": { + "description": "Ikuti [instruksi]({more_info_url}) untuk [layar persetujuan OAuth]({oauth_consent_url}) untuk memberikan akses Home Assistant ke Google Spreadsheet 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", + "create_spreadsheet_failure": "Kesalahan saat membuat spreadsheet, lihat log kesalahan untuk detailnya", + "invalid_access_token": "Token akses tidak valid", + "missing_configuration": "Komponen tidak dikonfigurasi. Ikuti petunjuk dalam dokumentasi.", + "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", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "create_entry": { + "default": "Berhasil mengautentikasi dan spreadsheet dibuat di: {url}" + }, + "step": { + "auth": { + "title": "Tautkan Akun Google" + }, + "pick_implementation": { + "title": "Pilih Metode Autentikasi" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google_sheets/translations/it.json b/homeassistant/components/google_sheets/translations/it.json new file mode 100644 index 00000000000..6d8f315a84a --- /dev/null +++ b/homeassistant/components/google_sheets/translations/it.json @@ -0,0 +1,31 @@ +{ + "application_credentials": { + "description": "Segui le [istruzioni]({more_info_url}) per la [schermata di consenso OAuth]({oauth_consent_url}) per consentire a Home Assistant di accedere ai tuoi Fogli Google. Devi anche creare le Credenziali dell'Applicazione collegate al tuo account:\n1. Vai a [Credenziali]({oauth_creds_url}) e fai clic su **Crea credenziali**.\n1. Dall'elenco a discesa seleziona **ID client OAuth**.\n1. Seleziona **Applicazione Web** per il 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", + "create_spreadsheet_failure": "Errore durante la creazione del foglio di calcolo, vedere il registro degli errori per i dettagli", + "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.", + "open_spreadsheet_failure": "Errore durante l'apertura del foglio di calcolo, vedere il registro degli errori per i dettagli", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente", + "timeout_connect": "Tempo scaduto per stabile la connessione.", + "unknown": "Errore imprevisto" + }, + "create_entry": { + "default": "Autenticato con successo e foglio di calcolo creato a: {url}" + }, + "step": { + "auth": { + "title": "Collega l'account Google" + }, + "pick_implementation": { + "title": "Scegli il metodo di autenticazione" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google_sheets/translations/ja.json b/homeassistant/components/google_sheets/translations/ja.json new file mode 100644 index 00000000000..e37b4517358 --- /dev/null +++ b/homeassistant/components/google_sheets/translations/ja.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "invalid_access_token": "\u7121\u52b9\u306a\u30a2\u30af\u30bb\u30b9\u30c8\u30fc\u30af\u30f3", + "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", + "oauth_error": "\u7121\u52b9\u306a\u30c8\u30fc\u30af\u30f3\u30c7\u30fc\u30bf\u3092\u53d7\u4fe1\u3057\u307e\u3057\u305f\u3002", + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f", + "timeout_connect": "\u63a5\u7d9a\u78ba\u7acb\u6642\u306b\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, + "step": { + "auth": { + "title": "Google\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u30ea\u30f3\u30af\u3059\u308b" + }, + "pick_implementation": { + "title": "\u8a8d\u8a3c\u65b9\u6cd5\u306e\u9078\u629e" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google_sheets/translations/nl.json b/homeassistant/components/google_sheets/translations/nl.json new file mode 100644 index 00000000000..d530b4e3add --- /dev/null +++ b/homeassistant/components/google_sheets/translations/nl.json @@ -0,0 +1,26 @@ +{ + "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" + }, + "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_sheets/translations/no.json b/homeassistant/components/google_sheets/translations/no.json new file mode 100644 index 00000000000..c4cec211828 --- /dev/null +++ b/homeassistant/components/google_sheets/translations/no.json @@ -0,0 +1,35 @@ +{ + "application_credentials": { + "description": "F\u00f8lg [instruksjonene]({more_info_url}) for [OAuth-samtykkeskjermen]({oauth_consent_url}) for \u00e5 gi Home Assistant tilgang til Google Regneark. Du m\u00e5 ogs\u00e5 opprette programlegitimasjon som er koblet til kontoen din:\n1. G\u00e5 til [Legitimasjon]({oauth_creds_url}) og klikk **Opprett legitimasjon**.\n1. Velg **OAuth-klient-ID** fra rullegardinlisten.\n1. Velg **Webprogram** for applikasjonstypen.\n\n" + }, + "config": { + "abort": { + "already_configured": "Kontoen er allerede konfigurert", + "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", + "cannot_connect": "Tilkobling mislyktes", + "create_spreadsheet_failure": "Feil under oppretting av regneark, se feillogg for detaljer", + "invalid_access_token": "Ugyldig tilgangstoken", + "missing_configuration": "Komponenten er ikke konfigurert, vennligst f\u00f8lg dokumentasjonen", + "oauth_error": "Mottatt ugyldige token data.", + "open_spreadsheet_failure": "Feil under \u00e5pning av regnearket, se feillogg for detaljer", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket", + "timeout_connect": "Tidsavbrudd oppretter forbindelse", + "unknown": "Uventet feil" + }, + "create_entry": { + "default": "Vellykket autentisert og regneark opprettet p\u00e5: {url}" + }, + "step": { + "auth": { + "title": "Koble til Google-kontoen" + }, + "pick_implementation": { + "title": "Velg godkjenningsmetode" + }, + "reauth_confirm": { + "description": "Google Regneark-integreringen m\u00e5 godkjenne kontoen din p\u00e5 nytt", + "title": "Godkjenne integrering p\u00e5 nytt" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google_sheets/translations/pl.json b/homeassistant/components/google_sheets/translations/pl.json new file mode 100644 index 00000000000..c976b9f1910 --- /dev/null +++ b/homeassistant/components/google_sheets/translations/pl.json @@ -0,0 +1,31 @@ +{ + "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 Arkuszy Google. 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", + "create_spreadsheet_failure": "B\u0142\u0105d podczas tworzenia arkusza kalkulacyjnego, szczeg\u00f3\u0142y b\u0142\u0119du znajdziesz w logach", + "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.", + "open_spreadsheet_failure": "B\u0142\u0105d podczas otwierania arkusza kalkulacyjnego, szczeg\u00f3\u0142y b\u0142\u0119du znajdziesz w logach", + "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 i utworzono arkusz kalkulacyjny pod adresem: {url}" + }, + "step": { + "auth": { + "title": "Po\u0142\u0105czenie z kontem Google" + }, + "pick_implementation": { + "title": "Wybierz metod\u0119 uwierzytelniania" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google_sheets/translations/pt-BR.json b/homeassistant/components/google_sheets/translations/pt-BR.json new file mode 100644 index 00000000000..e8b4f23a4ed --- /dev/null +++ b/homeassistant/components/google_sheets/translations/pt-BR.json @@ -0,0 +1,31 @@ +{ + "application_credentials": { + "description": "Siga as [instru\u00e7\u00f5es]( {more_info_url} ) para a [tela de consentimento do OAuth]( {oauth_consent_url} ) para conceder ao Home Assistant acesso as suas Planilhas Google. Voc\u00ea tamb\u00e9m precisa criar credenciais de aplicativo vinculadas \u00e0 sua conta:\n 1. Acesse [Credentials]( {oauth_creds_url} ) e clique em **Create Credentials**.\n 1. Na lista suspensa, selecione **ID do cliente OAuth**.\n 1. Selecione **Aplicativo da Web** para o Tipo de aplicativo. \n\n" + }, + "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", + "create_spreadsheet_failure": "Erro ao criar planilha, veja o log de erros para detalhes", + "invalid_access_token": "Token de acesso inv\u00e1lido", + "missing_configuration": "O componente n\u00e3o est\u00e1 configurado. Por favor, siga a documenta\u00e7\u00e3o.", + "oauth_error": "Dados de token inv\u00e1lidos recebidos.", + "open_spreadsheet_failure": "Erro ao abrir a planilha, veja o log de erros para detalhes", + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida", + "timeout_connect": "Tempo limite estabelecendo conex\u00e3o", + "unknown": "Erro inesperado" + }, + "create_entry": { + "default": "Autentica\u00e7\u00e3o com sucesso e planilha criada em: {url}" + }, + "step": { + "auth": { + "title": "Vincular Conta do Google" + }, + "pick_implementation": { + "title": "Escolha o m\u00e9todo de autentica\u00e7\u00e3o" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google_sheets/translations/pt.json b/homeassistant/components/google_sheets/translations/pt.json new file mode 100644 index 00000000000..60c19d18dc2 --- /dev/null +++ b/homeassistant/components/google_sheets/translations/pt.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "open_spreadsheet_failure": "Erro ao abrir a planilha, veja o log de erros para detalhes" + }, + "create_entry": { + "default": "Autentica\u00e7\u00e3o com sucesso e planilha criada em: {url}" + }, + "step": { + "auth": { + "title": "Vincular Conta do Google" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google_sheets/translations/ru.json b/homeassistant/components/google_sheets/translations/ru.json new file mode 100644 index 00000000000..71f9d0449d8 --- /dev/null +++ b/homeassistant/components/google_sheets/translations/ru.json @@ -0,0 +1,35 @@ +{ + "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." + }, + "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.", + "create_spreadsheet_failure": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0438 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u0442\u0430\u0431\u043b\u0438\u0446\u044b, \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0441\u0442\u0438 \u0441\u043c. \u0432 \u0436\u0443\u0440\u043d\u0430\u043b\u0435 \u043e\u0448\u0438\u0431\u043e\u043a.", + "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.", + "open_spreadsheet_failure": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u043e\u0442\u043a\u0440\u044b\u0442\u0438\u0438 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u0442\u0430\u0431\u043b\u0438\u0446\u044b, \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0441\u0442\u0438 \u0441\u043c. \u0432 \u0436\u0443\u0440\u043d\u0430\u043b\u0435 \u043e\u0448\u0438\u0431\u043e\u043a.", + "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\u0448\u043b\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e, \u0442\u0430\u0431\u043b\u0438\u0446\u0430 \u0441\u043e\u0437\u0434\u0430\u043d\u0430 \u043f\u043e \u0430\u0434\u0440\u0435\u0441\u0443: {url}" + }, + "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_sheets/translations/zh-Hant.json b/homeassistant/components/google_sheets/translations/zh-Hant.json new file mode 100644 index 00000000000..8afd7ddaf11 --- /dev/null +++ b/homeassistant/components/google_sheets/translations/zh-Hant.json @@ -0,0 +1,35 @@ +{ + "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 \u8868\u683c\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", + "create_spreadsheet_failure": "\u5efa\u7acb\u8868\u683c\u6642\u767c\u751f\u932f\u8aa4\u3001\u8acb\u53c3\u8003\u932f\u8aa4\u65e5\u8a8c\u4ee5\u7372\u5f97\u8a73\u7d30\u8cc7\u6599", + "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", + "open_spreadsheet_failure": "\u958b\u555f\u8868\u683c\u6642\u767c\u751f\u932f\u8aa4\u3001\u8acb\u53c3\u8003\u932f\u8aa4\u65e5\u8a8c\u4ee5\u7372\u5f97\u8a73\u7d30\u8cc7\u6599", + "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\u4e26\u65bc\u9023\u7d50\u4f4d\u7f6e\u5efa\u7acb\u8868\u683c\uff1a{url}" + }, + "step": { + "auth": { + "title": "\u9023\u7d50 Google \u5e33\u865f" + }, + "pick_implementation": { + "title": "\u9078\u64c7\u9a57\u8b49\u6a21\u5f0f" + }, + "reauth_confirm": { + "description": "Google Sheets \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/govee_ble/manifest.json b/homeassistant/components/govee_ble/manifest.json index 29af7502ded..1cb1eeaddd8 100644 --- a/homeassistant/components/govee_ble/manifest.json +++ b/homeassistant/components/govee_ble/manifest.json @@ -37,6 +37,11 @@ "service_uuid": "00008551-0000-1000-8000-00805f9b34fb", "connectable": false }, + { + "manufacturer_id": 53579, + "service_uuid": "00008151-0000-1000-8000-00805f9b34fb", + "connectable": false + }, { "manufacturer_id": 43682, "service_uuid": "00008151-0000-1000-8000-00805f9b34fb", @@ -68,7 +73,7 @@ "connectable": false } ], - "requirements": ["govee-ble==0.19.0"], + "requirements": ["govee-ble==0.19.1"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "iot_class": "local_push" diff --git a/homeassistant/components/govee_ble/translations/bg.json b/homeassistant/components/govee_ble/translations/bg.json new file mode 100644 index 00000000000..a61dac839ad --- /dev/null +++ b/homeassistant/components/govee_ble/translations/bg.json @@ -0,0 +1,20 @@ +{ + "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" + }, + "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/govee_ble/translations/cs.json b/homeassistant/components/govee_ble/translations/cs.json new file mode 100644 index 00000000000..925c1cbd337 --- /dev/null +++ b/homeassistant/components/govee_ble/translations/cs.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "already_in_progress": "Konfigurace ji\u017e prob\u00edh\u00e1", + "no_devices_found": "V s\u00edti nebyla nalezena \u017e\u00e1dn\u00e1 za\u0159\u00edzen\u00ed" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Chcete nastavit {name}?" + }, + "user": { + "data": { + "address": "Za\u0159\u00edzen\u00ed" + }, + "description": "Zvolte za\u0159\u00edzen\u00ed, kter\u00e9 chcete nastavit" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gree/climate.py b/homeassistant/components/gree/climate.py index 4096f58e4cb..976cc31761d 100644 --- a/homeassistant/components/gree/climate.py +++ b/homeassistant/components/gree/climate.py @@ -16,8 +16,7 @@ from greeclimate.device import ( VerticalSwing, ) -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( FAN_AUTO, FAN_HIGH, FAN_LOW, @@ -31,6 +30,7 @@ from homeassistant.components.climate.const import ( SWING_HORIZONTAL, SWING_OFF, SWING_VERTICAL, + ClimateEntity, ClimateEntityFeature, HVACMode, ) diff --git a/homeassistant/components/group/__init__.py b/homeassistant/components/group/__init__.py index 04bc109d15b..bdf295e35ae 100644 --- a/homeassistant/components/group/__init__.py +++ b/homeassistant/components/group/__init__.py @@ -3,10 +3,10 @@ from __future__ import annotations from abc import abstractmethod import asyncio -from collections.abc import Iterable +from collections.abc import Collection, Iterable from contextvars import ContextVar import logging -from typing import Any, Union, cast +from typing import Any, Protocol, cast import voluptuous as vol @@ -27,7 +27,15 @@ from homeassistant.const import ( STATE_ON, Platform, ) -from homeassistant.core import HomeAssistant, ServiceCall, callback, split_entity_id +from homeassistant.core import ( + CALLBACK_TYPE, + Event, + HomeAssistant, + ServiceCall, + State, + callback, + split_entity_id, +) from homeassistant.helpers import config_validation as cv, entity_registry as er, start from homeassistant.helpers.entity import Entity, async_generate_entity_id from homeassistant.helpers.entity_component import EntityComponent @@ -42,8 +50,6 @@ from homeassistant.loader import bind_hass from .const import CONF_HIDE_MEMBERS -# mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs - DOMAIN = "group" GROUP_ORDER = "group_order" @@ -79,10 +85,19 @@ _LOGGER = logging.getLogger(__name__) current_domain: ContextVar[str] = ContextVar("current_domain") -def _conf_preprocess(value): +class GroupProtocol(Protocol): + """Define the format of group platforms.""" + + def async_describe_on_off_states( + self, hass: HomeAssistant, registry: GroupIntegrationRegistry + ) -> None: + """Describe group on off states.""" + + +def _conf_preprocess(value: Any) -> dict[str, Any]: """Preprocess alternative configuration formats.""" if not isinstance(value, dict): - value = {CONF_ENTITIES: value} + return {CONF_ENTITIES: value} return value @@ -104,9 +119,9 @@ CONFIG_SCHEMA = vol.Schema( ) -def _async_get_component(hass: HomeAssistant) -> EntityComponent: +def _async_get_component(hass: HomeAssistant) -> EntityComponent[Group]: if (component := hass.data.get(DOMAIN)) is None: - component = hass.data[DOMAIN] = EntityComponent(_LOGGER, DOMAIN, hass) + component = hass.data[DOMAIN] = EntityComponent[Group](_LOGGER, DOMAIN, hass) return component @@ -135,14 +150,15 @@ class GroupIntegrationRegistry: @bind_hass -def is_on(hass, entity_id): +def is_on(hass: HomeAssistant, entity_id: str) -> bool: """Test if the group state is in its ON-state.""" if REG_KEY not in hass.data: # Integration not setup yet, it cannot be on return False if (state := hass.states.get(entity_id)) is not None: - return state.state in hass.data[REG_KEY].on_off_mapping + registry: GroupIntegrationRegistry = hass.data[REG_KEY] + return state.state in registry.on_off_mapping return False @@ -272,11 +288,11 @@ async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up all groups found defined in the configuration.""" if DOMAIN not in hass.data: - hass.data[DOMAIN] = EntityComponent(_LOGGER, DOMAIN, hass) + hass.data[DOMAIN] = EntityComponent[Group](_LOGGER, DOMAIN, hass) await async_process_integration_platform_for_component(hass, DOMAIN) - component: EntityComponent = hass.data[DOMAIN] + component: EntityComponent[Group] = hass.data[DOMAIN] hass.data[REG_KEY] = GroupIntegrationRegistry() @@ -286,11 +302,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def reload_service_handler(service: ServiceCall) -> None: """Remove all user-defined groups and load new ones from config.""" - auto = [ - cast(Group, e) - for e in component.entities - if not cast(Group, e).user_defined - ] + auto = [e for e in component.entities if not e.user_defined] if (conf := await component.async_prepare_reload()) is None: return @@ -315,7 +327,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Handle dynamic group service functions.""" object_id = service.data[ATTR_OBJECT_ID] entity_id = f"{DOMAIN}.{object_id}" - group: Group | None = cast(Union[Group, None], component.get_entity(entity_id)) + group = component.get_entity(entity_id) # new group if service.service == SERVICE_SET and group is None: @@ -408,10 +420,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return True -async def _process_group_platform(hass, domain, platform): +async def _process_group_platform( + hass: HomeAssistant, domain: str, platform: GroupProtocol +) -> None: """Process a group platform.""" current_domain.set(domain) - platform.async_describe_on_off_states(hass, hass.data[REG_KEY]) + registry: GroupIntegrationRegistry = hass.data[REG_KEY] + platform.async_describe_on_off_states(hass, registry) async def _async_process_config(hass: HomeAssistant, config: ConfigType) -> None: @@ -423,7 +438,7 @@ async def _async_process_config(hass: HomeAssistant, config: ConfigType) -> None for object_id, conf in domain_config.items(): name: str = conf.get(CONF_NAME, object_id) - entity_ids: Iterable[str] = conf.get(CONF_ENTITIES) or [] + entity_ids: Collection[str] = conf.get(CONF_ENTITIES) or [] icon: str | None = conf.get(CONF_ICON) mode = bool(conf.get(CONF_ALL)) order: int = hass.data[GROUP_ORDER] @@ -456,15 +471,12 @@ async def _async_process_config(hass: HomeAssistant, config: ConfigType) -> None class GroupEntity(Entity): """Representation of a Group of entities.""" - @property - def should_poll(self) -> bool: - """Disable polling for group.""" - return False + _attr_should_poll = False async def async_added_to_hass(self) -> None: """Register listeners.""" - async def _update_at_start(_): + async def _update_at_start(_: HomeAssistant) -> None: self.async_update_group_state() self.async_write_ha_state() @@ -487,6 +499,10 @@ class GroupEntity(Entity): class Group(Entity): """Track a group of entity ids.""" + _attr_should_poll = False + tracking: tuple[str, ...] + trackable: tuple[str, ...] + def __init__( self, hass: HomeAssistant, @@ -494,7 +510,7 @@ class Group(Entity): order: int | None = None, icon: str | None = None, user_defined: bool = True, - entity_ids: Iterable[str] | None = None, + entity_ids: Collection[str] | None = None, mode: bool | None = None, ) -> None: """Initialize a group. @@ -503,25 +519,25 @@ class Group(Entity): """ self.hass = hass self._name = name - self._state = None + self._state: str | None = None self._icon = icon self._set_tracked(entity_ids) - self._on_off = None - self._assumed = None - self._on_states = None + self._on_off: dict[str, bool] = {} + self._assumed: dict[str, bool] = {} + self._on_states: set[str] = set() self.user_defined = user_defined self.mode = any if mode: self.mode = all self._order = order self._assumed_state = False - self._async_unsub_state_changed = None + self._async_unsub_state_changed: CALLBACK_TYPE | None = None @staticmethod def create_group( hass: HomeAssistant, name: str, - entity_ids: Iterable[str] | None = None, + entity_ids: Collection[str] | None = None, user_defined: bool = True, icon: str | None = None, object_id: str | None = None, @@ -541,7 +557,7 @@ class Group(Entity): def async_create_group_entity( hass: HomeAssistant, name: str, - entity_ids: Iterable[str] | None = None, + entity_ids: Collection[str] | None = None, user_defined: bool = True, icon: str | None = None, object_id: str | None = None, @@ -577,7 +593,7 @@ class Group(Entity): async def async_create_group( hass: HomeAssistant, name: str, - entity_ids: Iterable[str] | None = None, + entity_ids: Collection[str] | None = None, user_defined: bool = True, icon: str | None = None, object_id: str | None = None, @@ -597,37 +613,32 @@ class Group(Entity): return group @property - def should_poll(self): - """No need to poll because groups will update themselves.""" - return False - - @property - def name(self): + def name(self) -> str: """Return the name of the group.""" return self._name @name.setter - def name(self, value): + def name(self, value: str) -> None: """Set Group name.""" self._name = value @property - def state(self): + def state(self) -> str | None: """Return the state of the group.""" return self._state @property - def icon(self): + def icon(self) -> str | None: """Return the icon of the group.""" return self._icon @icon.setter - def icon(self, value): + def icon(self, value: str | None) -> None: """Set Icon for group.""" self._icon = value @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes for the group.""" data = {ATTR_ENTITY_ID: self.tracking, ATTR_ORDER: self._order} if not self.user_defined: @@ -636,17 +647,19 @@ class Group(Entity): return data @property - def assumed_state(self): + def assumed_state(self) -> bool: """Test if any member has an assumed state.""" return self._assumed_state - def update_tracked_entity_ids(self, entity_ids): + def update_tracked_entity_ids(self, entity_ids: Collection[str] | None) -> None: """Update the member entity IDs.""" asyncio.run_coroutine_threadsafe( self.async_update_tracked_entity_ids(entity_ids), self.hass.loop ).result() - async def async_update_tracked_entity_ids(self, entity_ids): + async def async_update_tracked_entity_ids( + self, entity_ids: Collection[str] | None + ) -> None: """Update the member entity IDs. This method must be run in the event loop. @@ -656,7 +669,7 @@ class Group(Entity): self._reset_tracked_state() self._async_start() - def _set_tracked(self, entity_ids): + def _set_tracked(self, entity_ids: Collection[str] | None) -> None: """Tuple of entities to be tracked.""" # tracking are the entities we want to track # trackable are the entities we actually watch @@ -666,10 +679,11 @@ class Group(Entity): self.trackable = () return - excluded_domains = self.hass.data[REG_KEY].exclude_domains + registry: GroupIntegrationRegistry = self.hass.data[REG_KEY] + excluded_domains = registry.exclude_domains - tracking = [] - trackable = [] + tracking: list[str] = [] + trackable: list[str] = [] for ent_id in entity_ids: ent_id_lower = ent_id.lower() domain = split_entity_id(ent_id_lower)[0] @@ -681,14 +695,14 @@ class Group(Entity): self.tracking = tuple(tracking) @callback - def _async_start(self, *_): + def _async_start(self, _: HomeAssistant | None = None) -> None: """Start tracking members and write state.""" self._reset_tracked_state() self._async_start_tracking() self.async_write_ha_state() @callback - def _async_start_tracking(self): + def _async_start_tracking(self) -> None: """Start tracking members. This method must be run in the event loop. @@ -701,7 +715,7 @@ class Group(Entity): self._async_update_group_state() @callback - def _async_stop(self): + def _async_stop(self) -> None: """Unregister the group from Home Assistant. This method must be run in the event loop. @@ -711,20 +725,20 @@ class Group(Entity): self._async_unsub_state_changed = None @callback - def async_update_group_state(self): + def async_update_group_state(self) -> None: """Query all members and determine current group state.""" self._state = None self._async_update_group_state() - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Handle addition to Home Assistant.""" self.async_on_remove(start.async_at_start(self.hass, self._async_start)) - async def async_will_remove_from_hass(self): + async def async_will_remove_from_hass(self) -> None: """Handle removal from Home Assistant.""" self._async_stop() - async def _async_state_changed_listener(self, event): + async def _async_state_changed_listener(self, event: Event) -> None: """Respond to a member state changing. This method must be run in the event loop. @@ -742,7 +756,7 @@ class Group(Entity): self._async_update_group_state(new_state) self.async_write_ha_state() - def _reset_tracked_state(self): + def _reset_tracked_state(self) -> None: """Reset tracked state.""" self._on_off = {} self._assumed = {} @@ -752,13 +766,13 @@ class Group(Entity): if (state := self.hass.states.get(entity_id)) is not None: self._see_state(state) - def _see_state(self, new_state): + def _see_state(self, new_state: State) -> None: """Keep track of the the state.""" entity_id = new_state.entity_id domain = new_state.domain state = new_state.state - registry = self.hass.data[REG_KEY] - self._assumed[entity_id] = new_state.attributes.get(ATTR_ASSUMED_STATE) + registry: GroupIntegrationRegistry = self.hass.data[REG_KEY] + self._assumed[entity_id] = bool(new_state.attributes.get(ATTR_ASSUMED_STATE)) if domain not in registry.on_states_by_domain: # Handle the group of a group case @@ -769,12 +783,12 @@ class Group(Entity): self._on_off[entity_id] = state in registry.on_off_mapping else: entity_on_state = registry.on_states_by_domain[domain] - if domain in self.hass.data[REG_KEY].on_states_by_domain: + if domain in registry.on_states_by_domain: self._on_states.update(entity_on_state) self._on_off[entity_id] = state in entity_on_state @callback - def _async_update_group_state(self, tr_state=None): + def _async_update_group_state(self, tr_state: State | None = None) -> None: """Update group state. Optionally you can provide the only state changed since last update @@ -818,4 +832,5 @@ class Group(Entity): if group_is_on: self._state = on_state else: - self._state = self.hass.data[REG_KEY].on_off_mapping[on_state] + registry: GroupIntegrationRegistry = self.hass.data[REG_KEY] + self._state = registry.on_off_mapping[on_state] diff --git a/homeassistant/components/group/media_player.py b/homeassistant/components/group/media_player.py index 60cb37f46ba..ddb44072080 100644 --- a/homeassistant/components/group/media_player.py +++ b/homeassistant/components/group/media_player.py @@ -18,6 +18,7 @@ from homeassistant.components.media_player import ( SERVICE_PLAY_MEDIA, MediaPlayerEntity, MediaPlayerEntityFeature, + MediaPlayerState, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -37,8 +38,6 @@ from homeassistant.const import ( SERVICE_TURN_ON, SERVICE_VOLUME_MUTE, SERVICE_VOLUME_SET, - STATE_OFF, - STATE_ON, STATE_UNAVAILABLE, STATE_UNKNOWN, ) @@ -104,6 +103,7 @@ class MediaPlayerGroup(MediaPlayerEntity): """Representation of a Media Group.""" _attr_available: bool = False + _attr_should_poll = False def __init__(self, unique_id: str | None, name: str, entities: list[str]) -> None: """Initialize a Media Group entity.""" @@ -217,11 +217,6 @@ class MediaPlayerGroup(MediaPlayerEntity): """Flag supported features.""" return self._supported_features - @property - def should_poll(self) -> bool: - """No polling needed for a media group.""" - return False - @property def extra_state_attributes(self) -> dict: """Return the state attributes for the media group.""" @@ -408,13 +403,13 @@ class MediaPlayerGroup(MediaPlayerEntity): # Set as unknown if all members are unknown or unavailable self._state = None else: - off_values = (STATE_OFF, STATE_UNAVAILABLE, STATE_UNKNOWN) + off_values = {MediaPlayerState.OFF, STATE_UNAVAILABLE, STATE_UNKNOWN} if states.count(states[0]) == len(states): self._state = states[0] elif any(state for state in states if state not in off_values): - self._state = STATE_ON + self._state = MediaPlayerState.ON else: - self._state = STATE_OFF + self._state = MediaPlayerState.OFF supported_features = 0 if self._features[KEY_CLEAR_PLAYLIST]: diff --git a/homeassistant/components/group/notify.py b/homeassistant/components/group/notify.py index 97c019163e8..7c4bc0c65c4 100644 --- a/homeassistant/components/group/notify.py +++ b/homeassistant/components/group/notify.py @@ -1,7 +1,10 @@ """Group platform for notify component.""" +from __future__ import annotations + import asyncio -from collections.abc import Mapping +from collections.abc import Coroutine, Mapping from copy import deepcopy +from typing import Any import voluptuous as vol @@ -13,9 +16,9 @@ from homeassistant.components.notify import ( BaseNotificationService, ) from homeassistant.const import ATTR_SERVICE +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv - -# mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType CONF_SERVICES = "services" @@ -29,46 +32,50 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -def update(input_dict, update_source): +def update(input_dict: dict[str, Any], update_source: dict[str, Any]) -> dict[str, Any]: """Deep update a dictionary. Async friendly. """ for key, val in update_source.items(): if isinstance(val, Mapping): - recurse = update(input_dict.get(key, {}), val) + recurse = update(input_dict.get(key, {}), val) # type: ignore[arg-type] input_dict[key] = recurse else: input_dict[key] = update_source[key] return input_dict -async def async_get_service(hass, config, discovery_info=None): +async def async_get_service( + hass: HomeAssistant, + config: ConfigType, + discovery_info: DiscoveryInfoType | None = None, +) -> GroupNotifyPlatform: """Get the Group notification service.""" - return GroupNotifyPlatform(hass, config.get(CONF_SERVICES)) + return GroupNotifyPlatform(hass, config[CONF_SERVICES]) class GroupNotifyPlatform(BaseNotificationService): """Implement the notification service for the group notify platform.""" - def __init__(self, hass, entities): + def __init__(self, hass: HomeAssistant, entities: list[dict[str, Any]]) -> None: """Initialize the service.""" self.hass = hass self.entities = entities - async def async_send_message(self, message="", **kwargs): + async def async_send_message(self, message: str = "", **kwargs: Any) -> None: """Send message to all entities in the group.""" - payload = {ATTR_MESSAGE: message} + payload: dict[str, Any] = {ATTR_MESSAGE: message} payload.update({key: val for key, val in kwargs.items() if val}) - tasks = [] + tasks: list[Coroutine[Any, Any, bool | None]] = [] for entity in self.entities: sending_payload = deepcopy(payload.copy()) - if entity.get(ATTR_DATA) is not None: - update(sending_payload, entity.get(ATTR_DATA)) + if (data := entity.get(ATTR_DATA)) is not None: + update(sending_payload, data) tasks.append( self.hass.services.async_call( - DOMAIN, entity.get(ATTR_SERVICE), sending_payload + DOMAIN, entity[ATTR_SERVICE], sending_payload ) ) diff --git a/homeassistant/components/group/translations/bg.json b/homeassistant/components/group/translations/bg.json index dea9870e1b7..f39166c65d8 100644 --- a/homeassistant/components/group/translations/bg.json +++ b/homeassistant/components/group/translations/bg.json @@ -8,6 +8,9 @@ }, "title": "\u041d\u043e\u0432\u0430 \u0433\u0440\u0443\u043f\u0430" }, + "cover": { + "title": "\u0414\u043e\u0431\u0430\u0432\u044f\u043d\u0435 \u043d\u0430 \u0433\u0440\u0443\u043f\u0430" + }, "fan": { "data": { "name": "\u0418\u043c\u0435" @@ -30,7 +33,8 @@ "media_player": { "data": { "name": "\u0418\u043c\u0435 \u043d\u0430 \u0433\u0440\u0443\u043f\u0430" - } + }, + "title": "\u0414\u043e\u0431\u0430\u0432\u044f\u043d\u0435 \u043d\u0430 \u0433\u0440\u0443\u043f\u0430" }, "switch": { "data": { diff --git a/homeassistant/components/group/translations/hu.json b/homeassistant/components/group/translations/hu.json index ae455bafebe..37e6567c517 100644 --- a/homeassistant/components/group/translations/hu.json +++ b/homeassistant/components/group/translations/hu.json @@ -63,10 +63,10 @@ "description": "A csoportok lehet\u0151v\u00e9 teszik egy \u00faj entit\u00e1s l\u00e9trehoz\u00e1s\u00e1t, amely t\u00f6bb azonos t\u00edpus\u00fa entit\u00e1st k\u00e9pvisel.", "menu_options": { "binary_sensor": "Bin\u00e1ris \u00e9rz\u00e9kel\u0151 csoport", - "cover": "Red\u0151ny csoport", + "cover": "\u00c1rny\u00e9kol\u00f3 csoport", "fan": "Ventil\u00e1tor csoport", "light": "L\u00e1mpa csoport", - "lock": "Csoport z\u00e1rol\u00e1sa", + "lock": "Z\u00e1r csoport", "media_player": "M\u00e9dialej\u00e1tsz\u00f3 csoport", "switch": "Kapcsol\u00f3csoport" }, diff --git a/homeassistant/components/group/translations/ja.json b/homeassistant/components/group/translations/ja.json index 46fbbc7b22f..c3403eafe03 100644 --- a/homeassistant/components/group/translations/ja.json +++ b/homeassistant/components/group/translations/ja.json @@ -60,7 +60,7 @@ "title": "\u65b0\u3057\u3044\u30b0\u30eb\u30fc\u30d7" }, "user": { - "description": "\u30b0\u30eb\u30fc\u30d7\u306e\u7a2e\u985e\u3092\u9078\u629e", + "description": "\u30b0\u30eb\u30fc\u30d7\u3092\u4f7f\u7528\u3059\u308b\u3068\u3001\u540c\u3058\u30bf\u30a4\u30d7\u306e\u8907\u6570\u306e\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u3092\u8868\u3059\u65b0\u3057\u3044\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u3092\u4f5c\u6210\u3067\u304d\u307e\u3059\u3002", "menu_options": { "binary_sensor": "\u30d0\u30a4\u30ca\u30ea\u30fc\u30bb\u30f3\u30b5\u30fc\u30b0\u30eb\u30fc\u30d7", "cover": "\u30ab\u30d0\u30fc\u30b0\u30eb\u30fc\u30d7", diff --git a/homeassistant/components/growatt_server/translations/cs.json b/homeassistant/components/growatt_server/translations/cs.json index 31a4cdfdf03..2dcdd1e7c37 100644 --- a/homeassistant/components/growatt_server/translations/cs.json +++ b/homeassistant/components/growatt_server/translations/cs.json @@ -7,7 +7,9 @@ "user": { "data": { "name": "Jm\u00e9no", - "url": "URL" + "password": "Heslo", + "url": "URL", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" } } } diff --git a/homeassistant/components/gstreamer/media_player.py b/homeassistant/components/gstreamer/media_player.py index 87329bdbc66..2861dc4516b 100644 --- a/homeassistant/components/gstreamer/media_player.py +++ b/homeassistant/components/gstreamer/media_player.py @@ -13,12 +13,11 @@ from homeassistant.components.media_player import ( BrowseMedia, MediaPlayerEntity, MediaPlayerEntityFeature, -) -from homeassistant.components.media_player.browse_media import ( + MediaPlayerState, + MediaType, async_process_play_media_url, ) -from homeassistant.components.media_player.const import MEDIA_TYPE_MUSIC -from homeassistant.const import CONF_NAME, EVENT_HOMEASSISTANT_STOP, STATE_IDLE +from homeassistant.const import CONF_NAME, EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -58,6 +57,7 @@ def setup_platform( class GstreamerDevice(MediaPlayerEntity): """Representation of a Gstreamer device.""" + _attr_media_content_type = MediaType.MUSIC _attr_supported_features = ( MediaPlayerEntityFeature.VOLUME_SET | MediaPlayerEntityFeature.PLAY @@ -71,7 +71,7 @@ class GstreamerDevice(MediaPlayerEntity): """Initialize the Gstreamer device.""" self._player = player self._name = name or DOMAIN - self._state = STATE_IDLE + self._state = MediaPlayerState.IDLE self._volume = None self._duration = None self._uri = None @@ -104,7 +104,7 @@ class GstreamerDevice(MediaPlayerEntity): ) media_id = sourced_media.url - elif media_type != MEDIA_TYPE_MUSIC: + elif media_type != MediaType.MUSIC: _LOGGER.error("Invalid media type") return @@ -129,11 +129,6 @@ class GstreamerDevice(MediaPlayerEntity): """Content ID of currently playing media.""" return self._uri - @property - def content_type(self): - """Content type of currently playing media.""" - return MEDIA_TYPE_MUSIC - @property def name(self): """Return the name of the device.""" diff --git a/homeassistant/components/guardian/__init__.py b/homeassistant/components/guardian/__init__.py index a3cc7a0031b..63fc66f685d 100644 --- a/homeassistant/components/guardian/__init__.py +++ b/homeassistant/components/guardian/__init__.py @@ -47,8 +47,6 @@ DATA_PAIRED_SENSOR_MANAGER = "paired_sensor_manager" SERVICE_NAME_DISABLE_AP = "disable_ap" SERVICE_NAME_ENABLE_AP = "enable_ap" SERVICE_NAME_PAIR_SENSOR = "pair_sensor" -SERVICE_NAME_REBOOT = "reboot" -SERVICE_NAME_RESET_VALVE_DIAGNOSTICS = "reset_valve_diagnostics" SERVICE_NAME_UNPAIR_SENSOR = "unpair_sensor" SERVICE_NAME_UPGRADE_FIRMWARE = "upgrade_firmware" @@ -56,8 +54,6 @@ SERVICES = ( SERVICE_NAME_DISABLE_AP, SERVICE_NAME_ENABLE_AP, SERVICE_NAME_PAIR_SENSOR, - SERVICE_NAME_REBOOT, - SERVICE_NAME_RESET_VALVE_DIAGNOSTICS, SERVICE_NAME_UNPAIR_SENSOR, SERVICE_NAME_UPGRADE_FIRMWARE, ) @@ -238,11 +234,25 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @call_with_data async def async_disable_ap(call: ServiceCall, data: GuardianData) -> None: """Disable the onboard AP.""" + async_log_deprecated_service_call( + hass, + call, + "switch.turn_off", + f"switch.guardian_valve_controller_{entry.data[CONF_UID]}_onboard_ap", + "2022.12.0", + ) await data.client.wifi.disable_ap() @call_with_data async def async_enable_ap(call: ServiceCall, data: GuardianData) -> None: """Enable the onboard AP.""" + async_log_deprecated_service_call( + hass, + call, + "switch.turn_on", + f"switch.guardian_valve_controller_{entry.data[CONF_UID]}_onboard_ap", + "2022.12.0", + ) await data.client.wifi.enable_ap() @call_with_data @@ -252,32 +262,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await data.client.sensor.pair_sensor(uid) await data.paired_sensor_manager.async_pair_sensor(uid) - @call_with_data - async def async_reboot(call: ServiceCall, data: GuardianData) -> None: - """Reboot the valve controller.""" - async_log_deprecated_service_call( - hass, - call, - "button.press", - f"button.guardian_valve_controller_{data.entry.data[CONF_UID]}_reboot", - "2022.10.0", - ) - await data.client.system.reboot() - - @call_with_data - async def async_reset_valve_diagnostics( - call: ServiceCall, data: GuardianData - ) -> None: - """Fully reset system motor diagnostics.""" - async_log_deprecated_service_call( - hass, - call, - "button.press", - f"button.guardian_valve_controller_{data.entry.data[CONF_UID]}_reset_valve_diagnostics", - "2022.10.0", - ) - await data.client.valve.reset() - @call_with_data async def async_unpair_sensor(call: ServiceCall, data: GuardianData) -> None: """Remove a paired sensor.""" @@ -302,12 +286,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: SERVICE_PAIR_UNPAIR_SENSOR_SCHEMA, async_pair_sensor, ), - (SERVICE_NAME_REBOOT, SERVICE_BASE_SCHEMA, async_reboot), - ( - SERVICE_NAME_RESET_VALVE_DIAGNOSTICS, - SERVICE_BASE_SCHEMA, - async_reset_valve_diagnostics, - ), ( SERVICE_NAME_UNPAIR_SENSOR, SERVICE_PAIR_UNPAIR_SENSOR_SCHEMA, diff --git a/homeassistant/components/guardian/binary_sensor.py b/homeassistant/components/guardian/binary_sensor.py index 766e5d961e8..6425ecd46a6 100644 --- a/homeassistant/components/guardian/binary_sensor.py +++ b/homeassistant/components/guardian/binary_sensor.py @@ -4,6 +4,7 @@ from __future__ import annotations from dataclasses import dataclass from homeassistant.components.binary_sensor import ( + DOMAIN as BINARY_SENSOR_DOMAIN, BinarySensorDeviceClass, BinarySensorEntity, BinarySensorEntityDescription, @@ -27,7 +28,11 @@ from .const import ( DOMAIN, SIGNAL_PAIRED_SENSOR_COORDINATOR_ADDED, ) -from .util import GuardianDataUpdateCoordinator +from .util import ( + EntityDomainReplacementStrategy, + GuardianDataUpdateCoordinator, + async_finish_entity_domain_replacements, +) ATTR_CONNECTED_CLIENTS = "connected_clients" @@ -79,6 +84,21 @@ async def async_setup_entry( ) -> None: """Set up Guardian switches based on a config entry.""" data: GuardianData = hass.data[DOMAIN][entry.entry_id] + uid = entry.data[CONF_UID] + + async_finish_entity_domain_replacements( + hass, + entry, + ( + EntityDomainReplacementStrategy( + BINARY_SENSOR_DOMAIN, + f"{uid}_ap_enabled", + f"switch.guardian_valve_controller_{uid}_onboard_ap", + "2022.12.0", + remove_old_entity=False, + ), + ), + ) @callback def add_new_paired_sensor(uid: str) -> None: diff --git a/homeassistant/components/guardian/services.yaml b/homeassistant/components/guardian/services.yaml index 61cf709a31c..7415ac626a9 100644 --- a/homeassistant/components/guardian/services.yaml +++ b/homeassistant/components/guardian/services.yaml @@ -1,26 +1,4 @@ # Describes the format for available Elexa Guardians services -disable_ap: - name: Disable AP - description: Disable the device's onboard access point. - fields: - device_id: - name: Valve Controller - description: The valve controller whose AP should be disabled - required: true - selector: - device: - integration: guardian -enable_ap: - name: Enable AP - description: Enable the device's onboard access point. - fields: - device_id: - name: Valve Controller - description: The valve controller whose AP should be enabled - required: true - selector: - device: - integration: guardian pair_sensor: name: Pair Sensor description: Add a new paired sensor to the valve controller. diff --git a/homeassistant/components/guardian/strings.json b/homeassistant/components/guardian/strings.json index 1665cf9f678..683f13c8d36 100644 --- a/homeassistant/components/guardian/strings.json +++ b/homeassistant/components/guardian/strings.json @@ -20,12 +20,12 @@ }, "issues": { "deprecated_service": { - "title": "The {deprecated_service} service is being removed", + "title": "The {deprecated_service} service will be removed", "fix_flow": { "step": { "confirm": { - "title": "The {deprecated_service} service is being 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}`. Then, click SUBMIT below to mark this issue as resolved." + "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/guardian/switch.py b/homeassistant/components/guardian/switch.py index 870dd721843..6f5dd4d16b7 100644 --- a/homeassistant/components/guardian/switch.py +++ b/homeassistant/components/guardian/switch.py @@ -1,41 +1,86 @@ """Switches for the Elexa Guardian integration.""" from __future__ import annotations +from collections.abc import Awaitable, Callable from dataclasses import dataclass from typing import Any +from aioguardian import Client from aioguardian.errors import GuardianError from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import GuardianData, ValveControllerEntity, ValveControllerEntityDescription -from .const import API_VALVE_STATUS, DOMAIN +from .const import API_VALVE_STATUS, API_WIFI_STATUS, DOMAIN ATTR_AVG_CURRENT = "average_current" +ATTR_CONNECTED_CLIENTS = "connected_clients" ATTR_INST_CURRENT = "instantaneous_current" ATTR_INST_CURRENT_DDT = "instantaneous_current_ddt" +ATTR_STATION_CONNECTED = "station_connected" ATTR_TRAVEL_COUNT = "travel_count" +SWITCH_KIND_ONBOARD_AP = "onboard_ap" SWITCH_KIND_VALVE = "valve" +@dataclass +class SwitchDescriptionMixin: + """Define an entity description mixin for Guardian switches.""" + + off_action: Callable[[Client], Awaitable] + on_action: Callable[[Client], Awaitable] + + @dataclass class ValveControllerSwitchDescription( - SwitchEntityDescription, ValveControllerEntityDescription + SwitchEntityDescription, ValveControllerEntityDescription, SwitchDescriptionMixin ): """Describe a Guardian valve controller switch.""" +async def _async_disable_ap(client: Client) -> None: + """Disable the onboard AP.""" + await client.wifi.disable_ap() + + +async def _async_enable_ap(client: Client) -> None: + """Enable the onboard AP.""" + await client.wifi.enable_ap() + + +async def _async_close_valve(client: Client) -> None: + """Close the valve.""" + await client.valve.close() + + +async def _async_open_valve(client: Client) -> None: + """Open the valve.""" + await client.valve.open() + + VALVE_CONTROLLER_DESCRIPTIONS = ( + ValveControllerSwitchDescription( + key=SWITCH_KIND_ONBOARD_AP, + name="Onboard AP", + icon="mdi:wifi", + entity_category=EntityCategory.CONFIG, + api_category=API_WIFI_STATUS, + off_action=_async_disable_ap, + on_action=_async_enable_ap, + ), ValveControllerSwitchDescription( key=SWITCH_KIND_VALVE, name="Valve controller", icon="mdi:water", api_category=API_VALVE_STATUS, + off_action=_async_close_valve, + on_action=_async_open_valve, ), ) @@ -53,9 +98,7 @@ async def async_setup_entry( class ValveControllerSwitch(ValveControllerEntity, SwitchEntity): - """Define a switch to open/close the Guardian valve.""" - - entity_description: ValveControllerSwitchDescription + """Define a switch related to a Guardian valve controller.""" ON_STATES = { "start_opening", @@ -64,6 +107,8 @@ class ValveControllerSwitch(ValveControllerEntity, SwitchEntity): "opened", } + entity_description: ValveControllerSwitchDescription + def __init__( self, entry: ConfigEntry, @@ -73,23 +118,31 @@ class ValveControllerSwitch(ValveControllerEntity, SwitchEntity): """Initialize.""" super().__init__(entry, data.valve_controller_coordinators, description) - self._attr_is_on = True self._client = data.client @callback def _async_update_from_latest_data(self) -> None: """Update the entity.""" - self._attr_is_on = self.coordinator.data["state"] in self.ON_STATES - self._attr_extra_state_attributes.update( - { - ATTR_AVG_CURRENT: self.coordinator.data["average_current"], - ATTR_INST_CURRENT: self.coordinator.data["instantaneous_current"], - ATTR_INST_CURRENT_DDT: self.coordinator.data[ - "instantaneous_current_ddt" - ], - ATTR_TRAVEL_COUNT: self.coordinator.data["travel_count"], - } - ) + if self.entity_description.key == SWITCH_KIND_ONBOARD_AP: + self._attr_extra_state_attributes.update( + { + ATTR_CONNECTED_CLIENTS: self.coordinator.data.get("ap_clients"), + ATTR_STATION_CONNECTED: self.coordinator.data["station_connected"], + } + ) + self._attr_is_on = self.coordinator.data["ap_enabled"] + elif self.entity_description.key == SWITCH_KIND_VALVE: + self._attr_is_on = self.coordinator.data["state"] in self.ON_STATES + self._attr_extra_state_attributes.update( + { + ATTR_AVG_CURRENT: self.coordinator.data["average_current"], + ATTR_INST_CURRENT: self.coordinator.data["instantaneous_current"], + ATTR_INST_CURRENT_DDT: self.coordinator.data[ + "instantaneous_current_ddt" + ], + ATTR_TRAVEL_COUNT: self.coordinator.data["travel_count"], + } + ) async def async_turn_off(self, **kwargs: Any) -> None: """Turn the switch off.""" @@ -98,9 +151,11 @@ class ValveControllerSwitch(ValveControllerEntity, SwitchEntity): try: async with self._client: - await self._client.valve.close() + await self.entity_description.off_action(self._client) except GuardianError as err: - raise HomeAssistantError(f"Error while closing the valve: {err}") from err + raise HomeAssistantError( + f'Error while turning "{self.entity_id}" off: {err}' + ) from err self._attr_is_on = False self.async_write_ha_state() @@ -112,9 +167,11 @@ class ValveControllerSwitch(ValveControllerEntity, SwitchEntity): try: async with self._client: - await self._client.valve.open() + await self.entity_description.on_action(self._client) except GuardianError as err: - raise HomeAssistantError(f"Error while opening the valve: {err}") from err + raise HomeAssistantError( + f'Error while turning "{self.entity_id}" on: {err}' + ) from err self._attr_is_on = True self.async_write_ha_state() diff --git a/homeassistant/components/guardian/translations/bg.json b/homeassistant/components/guardian/translations/bg.json index de9699e4a21..d48caec927f 100644 --- a/homeassistant/components/guardian/translations/bg.json +++ b/homeassistant/components/guardian/translations/bg.json @@ -12,5 +12,28 @@ } } } + }, + "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" + } + } + }, + "title": "\u0423\u0441\u043b\u0443\u0433\u0430\u0442\u0430 {deprecated_service} \u0449\u0435 \u0431\u044a\u0434\u0435 \u043f\u0440\u0435\u043c\u0430\u0445\u043d\u0430\u0442\u0430" + }, + "replaced_old_entity": { + "fix_flow": { + "step": { + "confirm": { + "description": "\u0410\u043a\u0442\u0443\u0430\u043b\u0438\u0437\u0438\u0440\u0430\u0439\u0442\u0435 \u0432\u0441\u0438\u0447\u043a\u0438 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0438\u0438 \u0438\u043b\u0438 \u0441\u043a\u0440\u0438\u043f\u0442\u043e\u0432\u0435, \u043a\u043e\u0438\u0442\u043e \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u0442 \u0442\u043e\u0437\u0438 \u043e\u0431\u0435\u043a\u0442, \u0442\u0430\u043a\u0430 \u0447\u0435 \u0432\u043c\u0435\u0441\u0442\u043e \u043d\u0435\u0433\u043e \u0434\u0430 \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u0442 `{replacement_entity_id}`.", + "title": "\u041e\u0431\u0435\u043a\u0442\u044a\u0442 {old_entity_id} \u0449\u0435 \u0431\u044a\u0434\u0435 \u043f\u0440\u0435\u043c\u0430\u0445\u043d\u0430\u0442" + } + } + }, + "title": "\u041e\u0431\u0435\u043a\u0442\u044a\u0442 {old_entity_id} \u0449\u0435 \u0431\u044a\u0434\u0435 \u043f\u0440\u0435\u043c\u0430\u0445\u043d\u0430\u0442" + } } } \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/ca.json b/homeassistant/components/guardian/translations/ca.json index a338db67446..dee6614924d 100644 --- a/homeassistant/components/guardian/translations/ca.json +++ b/homeassistant/components/guardian/translations/ca.json @@ -23,12 +23,23 @@ "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}`. Despr\u00e9s, fes clic a ENVIAR per marcar aquest problema com a resolt.", - "title": "El servei {deprecated_service} est\u00e0 sent eliminat" + "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} ser\u00e0 eliminat" } } }, - "title": "El servei {deprecated_service} est\u00e0 sent eliminat" + "title": "El servei {deprecated_service} ser\u00e0 eliminat" + }, + "replaced_old_entity": { + "fix_flow": { + "step": { + "confirm": { + "description": "Actualitza totes les automatitzacions o 'scripts' que utilitzin aquesta entitat perqu\u00e8 passin a utilitzar l'entitat `{replacement_entity_id}`.", + "title": "L'entitat {old_entity_id} s'eliminar\u00e0" + } + } + }, + "title": "L'entitat {old_entity_id} s'eliminar\u00e0" } } } \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/de.json b/homeassistant/components/guardian/translations/de.json index 33d2973317f..9d5c0c0265c 100644 --- a/homeassistant/components/guardian/translations/de.json +++ b/homeassistant/components/guardian/translations/de.json @@ -23,12 +23,23 @@ "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. Dr\u00fccke dann unten auf SENDEN, um dieses Problem als behoben zu markieren.", + "description": "Aktualisiere alle Automatisierungen oder Skripte, die diesen Dienst verwenden, um stattdessen den Dienst `{alternate_service}` mit einer Ziel-Entit\u00e4ts-ID von `{alternate_target}` zu verwenden.", "title": "Der Dienst {deprecated_service} wird entfernt" } } }, "title": "Der Dienst {deprecated_service} wird entfernt" + }, + "replaced_old_entity": { + "fix_flow": { + "step": { + "confirm": { + "description": "Aktualisiere alle Automatisierungen oder Skripte, die diese Entit\u00e4t verwenden, um stattdessen `{replacement_entity_id}` zu verwenden.", + "title": "Die Entit\u00e4t {old_entity_id} wird entfernt" + } + } + }, + "title": "Die Entit\u00e4t {old_entity_id} wird entfernt" } } } \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/el.json b/homeassistant/components/guardian/translations/el.json index 2a4963c8649..0857e0284a4 100644 --- a/homeassistant/components/guardian/translations/el.json +++ b/homeassistant/components/guardian/translations/el.json @@ -29,6 +29,17 @@ } }, "title": "\u0397 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 {deprecated_service} \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9" + }, + "replaced_old_entity": { + "fix_flow": { + "step": { + "confirm": { + "description": "\u0395\u03bd\u03b7\u03bc\u03b5\u03c1\u03ce\u03c3\u03c4\u03b5 \u03cc\u03bb\u03bf\u03c5\u03c2 \u03c4\u03bf\u03c5\u03c2 \u03b1\u03c5\u03c4\u03bf\u03bc\u03b1\u03c4\u03b9\u03c3\u03bc\u03bf\u03cd\u03c2 \u03ae \u03c4\u03b1 \u03c3\u03b5\u03bd\u03ac\u03c1\u03b9\u03b1 \u03c0\u03bf\u03c5 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03bf\u03cd\u03bd \u03b1\u03c5\u03c4\u03ae \u03c4\u03b7\u03bd \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1 \u03ce\u03c3\u03c4\u03b5 \u03bd\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03bf\u03cd\u03bd \u03b1\u03bd\u03c4\u03af \u03b3\u03b9\u03b1 \u03b1\u03c5\u03c4\u03ae\u03bd \u03c4\u03bf `{replacement_entity_id}`.", + "title": "\u0397 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1 {old_entity_id} \u03b8\u03b1 \u03b1\u03c6\u03b1\u03b9\u03c1\u03b5\u03b8\u03b5\u03af" + } + } + }, + "title": "\u0397 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1 {old_entity_id} \u03b8\u03b1 \u03b1\u03c6\u03b1\u03b9\u03c1\u03b5\u03b8\u03b5\u03af" } } } \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/en.json b/homeassistant/components/guardian/translations/en.json index ad6d0a4b7dc..1aaf8b888c8 100644 --- a/homeassistant/components/guardian/translations/en.json +++ b/homeassistant/components/guardian/translations/en.json @@ -23,12 +23,12 @@ "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}`. Then, click SUBMIT below to mark this issue as resolved.", - "title": "The {deprecated_service} service is being 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}`.", + "title": "The {deprecated_service} service will be removed" } } }, - "title": "The {deprecated_service} service is being removed" + "title": "The {deprecated_service} service will be removed" } } } \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/es.json b/homeassistant/components/guardian/translations/es.json index 93bccfbd03d..16233859836 100644 --- a/homeassistant/components/guardian/translations/es.json +++ b/homeassistant/components/guardian/translations/es.json @@ -23,12 +23,23 @@ "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 un ID de entidad de destino de `{alternate_target}`. Luego, haz clic en ENVIAR a continuaci\u00f3n para marcar este problema como resuelto.", + "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 va a eliminar el servicio {deprecated_service}" } } }, "title": "Se va a eliminar el servicio {deprecated_service}" + }, + "replaced_old_entity": { + "fix_flow": { + "step": { + "confirm": { + "description": "Actualiza cualquier automatizaci\u00f3n o script que use esta entidad para usar `{replacement_entity_id}`.", + "title": "Se eliminar\u00e1 la entidad {old_entity_id}" + } + } + }, + "title": "Se eliminar\u00e1 la entidad {old_entity_id}" } } } \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/et.json b/homeassistant/components/guardian/translations/et.json index 49172263d9c..37eee5acf33 100644 --- a/homeassistant/components/guardian/translations/et.json +++ b/homeassistant/components/guardian/translations/et.json @@ -29,6 +29,17 @@ } }, "title": "Teenus {deprecated_service} eemaldatakse" + }, + "replaced_old_entity": { + "fix_flow": { + "step": { + "confirm": { + "description": "See olem on asendatud olemiga \"{replacement_entity_id}\".", + "title": "Olem {old_entity_id} eemaldatakse" + } + } + }, + "title": "Olem {old_entity_id} eemaldatakse" } } } \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/fr.json b/homeassistant/components/guardian/translations/fr.json index d76ec4886e3..253d28ec770 100644 --- a/homeassistant/components/guardian/translations/fr.json +++ b/homeassistant/components/guardian/translations/fr.json @@ -17,5 +17,29 @@ "description": "Configurez un appareil Elexa Guardian local." } } + }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "Modifiez tout script ou automatisation utilisant ce service afin qu'ils utilisent \u00e0 la place le service `{alternate_service}` avec pour ID d'entit\u00e9 cible `{alternate_target}`.", + "title": "Le service {deprecated_service} sera bient\u00f4t supprim\u00e9" + } + } + }, + "title": "Le service {deprecated_service} sera bient\u00f4t supprim\u00e9" + }, + "replaced_old_entity": { + "fix_flow": { + "step": { + "confirm": { + "description": "Modifiez tout script ou automatisation utilisant cette entit\u00e9 afin qu'ils utilisent `{replacement_entity_id}` \u00e0 la place.", + "title": "L'entit\u00e9 {old_entity_id} sera supprim\u00e9e" + } + } + }, + "title": "L'entit\u00e9 {old_entity_id} sera supprim\u00e9e" + } } } \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/hu.json b/homeassistant/components/guardian/translations/hu.json index 35787e95524..20b5d00e2f1 100644 --- a/homeassistant/components/guardian/translations/hu.json +++ b/homeassistant/components/guardian/translations/hu.json @@ -23,12 +23,23 @@ "fix_flow": { "step": { "confirm": { - "description": "Friss\u00edtsen minden olyan automatiz\u00e1l\u00e1st vagy szkriptet, amely ezt a szolg\u00e1ltat\u00e1st haszn\u00e1lja, hogy helyette a(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\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": "{deprecated_service} szolg\u00e1ltat\u00e1s elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" } } }, "title": "{deprecated_service} szolg\u00e1ltat\u00e1s elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" + }, + "replaced_old_entity": { + "fix_flow": { + "step": { + "confirm": { + "description": "Friss\u00edtse az ezt az entit\u00e1st haszn\u00e1l\u00f3 automatiz\u00e1l\u00e1sokat vagy szkripteket, hogy helyette a k\u00f6vetkez\u0151t haszn\u00e1ja: `{replacement_entity_id}`", + "title": "{old_entity_id} entit\u00e1s el lesz t\u00e1vol\u00edtva" + } + } + }, + "title": "{old_entity_id} entit\u00e1s el lesz t\u00e1vol\u00edtva" } } } \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/id.json b/homeassistant/components/guardian/translations/id.json index 0d06eb61729..e62f48eae8f 100644 --- a/homeassistant/components/guardian/translations/id.json +++ b/homeassistant/components/guardian/translations/id.json @@ -23,12 +23,23 @@ "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}`. Kemudian, klik KIRIM di bawah ini untuk menandai masalah ini sebagai terselesaikan.", - "title": "Layanan {deprecated_service} dalam proses penghapusan" + "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} dalam proses penghapusan" + "title": "Layanan {deprecated_service} akan dihapus" + }, + "replaced_old_entity": { + "fix_flow": { + "step": { + "confirm": { + "description": "Perbarui setiap otomasi atau skrip yang menggunakan entitas ini untuk menggunakan `{replacement_entity_id}`.", + "title": "Entitas {old_entity_id} akan dihapus" + } + } + }, + "title": "Entitas {old_entity_id} akan dihapus" } } } \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/it.json b/homeassistant/components/guardian/translations/it.json index 4f43ace1b71..29c1d90ed87 100644 --- a/homeassistant/components/guardian/translations/it.json +++ b/homeassistant/components/guardian/translations/it.json @@ -23,12 +23,23 @@ "fix_flow": { "step": { "confirm": { - "description": "Aggiorna tutte le automazioni o gli script che utilizzano questo servizio e utilizzare invece il servizio `{alternate_service}` con un ID entit\u00e0 di destinazione di `{alternate_target}`. Quindi, fai clic su INVIA di seguito per contrassegnare questo problema come risolto.", + "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} verr\u00e0 rimosso" } } }, "title": "Il servizio {deprecated_service} verr\u00e0 rimosso" + }, + "replaced_old_entity": { + "fix_flow": { + "step": { + "confirm": { + "description": "Aggiorna tutte le automazioni o gli script che utilizzano questa entit\u00e0 in modo che utilizzino invece `{replacement_entity_id}`.", + "title": "L'entit\u00e0 {old_entity_id} verr\u00e0 rimossa" + } + } + }, + "title": "L'entit\u00e0 {old_entity_id} verr\u00e0 rimossa" } } } \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/ja.json b/homeassistant/components/guardian/translations/ja.json index 7e95869e071..96e66212b12 100644 --- a/homeassistant/components/guardian/translations/ja.json +++ b/homeassistant/components/guardian/translations/ja.json @@ -23,6 +23,7 @@ "fix_flow": { "step": { "confirm": { + "description": "\u3053\u306e\u30b5\u30fc\u30d3\u30b9\u3092\u4f7f\u7528\u3059\u308b\u3059\u3079\u3066\u306e\u81ea\u52d5\u5316\u307e\u305f\u306f\u30b9\u30af\u30ea\u30d7\u30c8\u3092\u66f4\u65b0\u3057\u3066\u3001\u4ee3\u308f\u308a\u306b ` {alternate_service} ` \u30b5\u30fc\u30d3\u30b9\u3092\u4f7f\u7528\u3057\u3001\u30bf\u30fc\u30b2\u30c3\u30c8 \u30a8\u30f3\u30c6\u30a3\u30c6\u30a3 ID \u3092 ` {alternate_target} ` \u306b\u3057\u307e\u3059\u3002\u6b21\u306b\u3001\u4e0b\u306e [\u9001\u4fe1] \u3092\u30af\u30ea\u30c3\u30af\u3057\u3066\u3001\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u6e08\u307f\u3068\u3057\u3066\u30de\u30fc\u30af\u3057\u307e\u3059\u3002", "title": "{deprecated_service} \u30b5\u30fc\u30d3\u30b9\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" } } diff --git a/homeassistant/components/guardian/translations/no.json b/homeassistant/components/guardian/translations/no.json index 9c2669fdeb2..b550bf9c43d 100644 --- a/homeassistant/components/guardian/translations/no.json +++ b/homeassistant/components/guardian/translations/no.json @@ -23,12 +23,23 @@ "fix_flow": { "step": { "confirm": { - "description": "Oppdater eventuelle automatiseringer eller skript som bruker denne tjenesten til i stedet \u00e5 bruke ` {alternate_service} `-tjenesten med en m\u00e5lenhets-ID p\u00e5 ` {alternate_target} `. Klikk deretter SEND nedenfor for \u00e5 merke dette problemet som l\u00f8st.", - "title": "{deprecated_service} -tjenesten blir fjernet" + "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 blir fjernet" + "title": "{deprecated_service} -tjenesten vil bli fjernet" + }, + "replaced_old_entity": { + "fix_flow": { + "step": { + "confirm": { + "description": "Oppdater eventuelle automatiseringer eller skript som bruker denne enheten til i stedet \u00e5 bruke ` {replacement_entity_id} `.", + "title": "{old_entity_id} vil bli fjernet" + } + } + }, + "title": "{old_entity_id} vil bli fjernet" } } } \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/pl.json b/homeassistant/components/guardian/translations/pl.json index c86f98b3b8e..f1cb2893050 100644 --- a/homeassistant/components/guardian/translations/pl.json +++ b/homeassistant/components/guardian/translations/pl.json @@ -23,12 +23,23 @@ "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}`. Nast\u0119pnie kliknij ZATWIERD\u0179 poni\u017cej, aby oznaczy\u0107 ten problem jako rozwi\u0105zany.", + "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" + }, + "replaced_old_entity": { + "fix_flow": { + "step": { + "confirm": { + "description": "Zaktualizuj wszystkie automatyzacje lub skrypty, kt\u00f3re u\u017cywaj\u0105 tej encji, aby zamiast tego u\u017cywa\u0142y `{replacement_entity_id}`.", + "title": "Encja {old_entity_id} zostanie usuni\u0119ta" + } + } + }, + "title": "Encja {old_entity_id} zostanie usuni\u0119ta" } } } \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/pt-BR.json b/homeassistant/components/guardian/translations/pt-BR.json index 2a4514f4968..53e3c724b7c 100644 --- a/homeassistant/components/guardian/translations/pt-BR.json +++ b/homeassistant/components/guardian/translations/pt-BR.json @@ -23,12 +23,23 @@ "fix_flow": { "step": { "confirm": { - "description": "Atualize quaisquer automa\u00e7\u00f5es ou scripts que usam este servi\u00e7o para usar o servi\u00e7o `{alternate_service}` com um ID de entidade de destino de `{alternate_target}`. Em seguida, clique em ENVIAR abaixo para marcar este problema como resolvido.", - "title": "O servi\u00e7o {deprecated_service} est\u00e1 sendo removido" + "description": "Atualize quaisquer automa\u00e7\u00f5es ou scripts que usam este 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} est\u00e1 sendo removido" + "title": "O servi\u00e7o {deprecated_service} ser\u00e1 removido" + }, + "replaced_old_entity": { + "fix_flow": { + "step": { + "confirm": { + "description": "Atualize quaisquer automa\u00e7\u00f5es ou scripts que usam essa entidade para usar `{replacement_entity_id}`.", + "title": "A entidade {old_entity_id} ser\u00e1 removida" + } + } + }, + "title": "A entidade {old_entity_id} ser\u00e1 removida" } } } \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/pt.json b/homeassistant/components/guardian/translations/pt.json index 91def9afb9d..04a5519b895 100644 --- a/homeassistant/components/guardian/translations/pt.json +++ b/homeassistant/components/guardian/translations/pt.json @@ -13,5 +13,18 @@ } } } + }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "Atualize quaisquer automa\u00e7\u00f5es ou scripts que usam este servi\u00e7o para usar o servi\u00e7o ` {alternate_service} ` com um ID de entidade de destino de ` {alternate_target} `. Em seguida, clique em ENVIAR abaixo para marcar este problema como resolvido.", + "title": "O servi\u00e7o {deprecated_service} est\u00e1 sendo removido" + } + } + }, + "title": "O servi\u00e7o {deprecated_service} est\u00e1 sendo removido" + } } } \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/ru.json b/homeassistant/components/guardian/translations/ru.json index 068787b5e86..6cfc857f0de 100644 --- a/homeassistant/components/guardian/translations/ru.json +++ b/homeassistant/components/guardian/translations/ru.json @@ -23,12 +23,23 @@ "fix_flow": { "step": { "confirm": { - "description": "\u0412\u043c\u0435\u0441\u0442\u043e \u044d\u0442\u043e\u0439 \u0441\u043b\u0443\u0436\u0431\u044b \u0442\u0435\u043f\u0435\u0440\u044c \u0441\u043b\u0435\u0434\u0443\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0441\u043b\u0443\u0436\u0431\u0443 `{alternate_service}` \u0441 \u0446\u0435\u043b\u0435\u0432\u044b\u043c \u043e\u0431\u044a\u0435\u043a\u0442\u043e\u043c `{alternate_target}`. \u041e\u0442\u0440\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u0443\u0439\u0442\u0435 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0438\u0438 \u0438 \u0441\u043a\u0440\u0438\u043f\u0442\u044b \u0438 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 \u043a\u043d\u043e\u043f\u043a\u0443 \u041f\u041e\u0414\u0422\u0412\u0415\u0420\u0414\u0418\u0422\u042c, \u0447\u0442\u043e\u0431\u044b \u043e\u0442\u043c\u0435\u0442\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443 \u043a\u0430\u043a \u0443\u0441\u0442\u0440\u0430\u043d\u0451\u043d\u043d\u0443\u044e.", + "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" + }, + "replaced_old_entity": { + "fix_flow": { + "step": { + "confirm": { + "description": "\u0412 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0438\u044f\u0445 \u0438 \u0441\u043a\u0440\u0438\u043f\u0442\u0430\u0445, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044e\u0449\u0438\u0445 \u044d\u0442\u043e\u0442 \u043e\u0431\u044a\u0435\u043a\u0442, \u0442\u0435\u043f\u0435\u0440\u044c \u0441\u043b\u0435\u0434\u0443\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043e\u0431\u044a\u0435\u043a\u0442 `{replacement_entity_id}`.", + "title": "\u041e\u0431\u044a\u0435\u043a\u0442 {old_entity_id} \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d" + } + } + }, + "title": "\u041e\u0431\u044a\u0435\u043a\u0442 {old_entity_id} \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d" } } } \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/sv.json b/homeassistant/components/guardian/translations/sv.json index 54fcf49904d..af41cc85efe 100644 --- a/homeassistant/components/guardian/translations/sv.json +++ b/homeassistant/components/guardian/translations/sv.json @@ -17,5 +17,18 @@ "description": "Konfigurera en lokal Elexa Guardian-enhet." } } + }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "Uppdatera alla automatiseringar eller skript som anv\u00e4nder den h\u00e4r tj\u00e4nsten f\u00f6r att ist\u00e4llet anv\u00e4nda tj\u00e4nsten ` {alternate_service} ` med ett m\u00e5lenhets-ID p\u00e5 ` {alternate_target} `. Klicka sedan p\u00e5 SKICKA nedan f\u00f6r att markera problemet som l\u00f6st.", + "title": "Tj\u00e4nsten {deprecated_service} tas bort" + } + } + }, + "title": "Tj\u00e4nsten {deprecated_service} tas bort" + } } } \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/zh-Hant.json b/homeassistant/components/guardian/translations/zh-Hant.json index baa0e477b4c..bd30a848b35 100644 --- a/homeassistant/components/guardian/translations/zh-Hant.json +++ b/homeassistant/components/guardian/translations/zh-Hant.json @@ -23,12 +23,23 @@ "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\uff0c\u7136\u5f8c\u9ede\u9078\u50b3\u9001\u4ee5\u6a19\u793a\u554f\u984c\u5df2\u89e3\u6c7a\u3002", - "title": "{deprecated_service} \u670d\u52d9\u5373\u5c07\u79fb\u9664" + "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\u5373\u5c07\u79fb\u9664" + "title": "{deprecated_service} \u670d\u52d9\u5c07\u79fb\u9664" + }, + "replaced_old_entity": { + "fix_flow": { + "step": { + "confirm": { + "description": "\u4f7f\u7528\u6b64\u5be6\u9ad4\u4ee5\u66f4\u65b0\u4efb\u4f55\u81ea\u52d5\u5316\u6216\u8173\u672c\uff0c\u4ee5\u53d6\u4ee3 `{replacement_entity_id}`\u3002", + "title": "{old_entity_id} \u5be6\u9ad4\u5c07\u9032\u884c\u79fb\u9664" + } + } + }, + "title": "{old_entity_id} \u5be6\u9ad4\u5c07\u9032\u884c\u79fb\u9664" } } } \ No newline at end of file diff --git a/homeassistant/components/guardian/util.py b/homeassistant/components/guardian/util.py index c88d6762e51..250fee58db5 100644 --- a/homeassistant/components/guardian/util.py +++ b/homeassistant/components/guardian/util.py @@ -2,7 +2,8 @@ from __future__ import annotations import asyncio -from collections.abc import Awaitable, Callable +from collections.abc import Awaitable, Callable, Iterable +from dataclasses import dataclass from datetime import timedelta from typing import Any, cast @@ -11,6 +12,7 @@ from aioguardian.errors import GuardianError from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import entity_registry from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -21,6 +23,43 @@ DEFAULT_UPDATE_INTERVAL = timedelta(seconds=30) SIGNAL_REBOOT_REQUESTED = "guardian_reboot_requested_{0}" +@dataclass +class EntityDomainReplacementStrategy: + """Define an entity replacement.""" + + old_domain: str + old_unique_id: str + replacement_entity_id: str + breaks_in_ha_version: str + remove_old_entity: bool = True + + +@callback +def async_finish_entity_domain_replacements( + hass: HomeAssistant, + entry: ConfigEntry, + entity_replacement_strategies: Iterable[EntityDomainReplacementStrategy], +) -> None: + """Remove old entities and create a repairs issue with info on their replacement.""" + ent_reg = entity_registry.async_get(hass) + for strategy in entity_replacement_strategies: + try: + [registry_entry] = [ + registry_entry + for registry_entry in ent_reg.entities.values() + if registry_entry.config_entry_id == entry.entry_id + and registry_entry.domain == strategy.old_domain + and registry_entry.unique_id == strategy.old_unique_id + ] + except ValueError: + continue + + old_entity_id = registry_entry.entity_id + if strategy.remove_old_entity: + LOGGER.info('Removing old entity: "%s"', old_entity_id) + ent_reg.async_remove(old_entity_id) + + class GuardianDataUpdateCoordinator(DataUpdateCoordinator[dict]): """Define an extended DataUpdateCoordinator with some Guardian goodies.""" diff --git a/homeassistant/components/hardware/manifest.json b/homeassistant/components/hardware/manifest.json index 94571ce4528..8f7e27e6911 100644 --- a/homeassistant/components/hardware/manifest.json +++ b/homeassistant/components/hardware/manifest.json @@ -4,5 +4,7 @@ "config_flow": false, "documentation": "https://www.home-assistant.io/integrations/hardware", "codeowners": ["@home-assistant/core"], - "requirements": ["psutil-home-assistant==0.0.1"] + "quality_scale": "internal", + "requirements": ["psutil-home-assistant==0.0.1"], + "integration_type": "system" } diff --git a/homeassistant/components/harman_kardon_avr/media_player.py b/homeassistant/components/harman_kardon_avr/media_player.py index c6272626f94..f222d4bd739 100644 --- a/homeassistant/components/harman_kardon_avr/media_player.py +++ b/homeassistant/components/harman_kardon_avr/media_player.py @@ -8,8 +8,9 @@ from homeassistant.components.media_player import ( PLATFORM_SCHEMA, MediaPlayerEntity, MediaPlayerEntityFeature, + MediaPlayerState, ) -from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, STATE_OFF, STATE_ON +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -72,9 +73,9 @@ class HkAvrDevice(MediaPlayerEntity): def update(self) -> None: """Update the state of this media_player.""" if self._avr.is_on(): - self._state = STATE_ON + self._state = MediaPlayerState.ON elif self._avr.is_off(): - self._state = STATE_OFF + self._state = MediaPlayerState.OFF else: self._state = None diff --git a/homeassistant/components/hassio/auth.py b/homeassistant/components/hassio/auth.py index 37687ee70df..afe944d03bc 100644 --- a/homeassistant/components/hassio/auth.py +++ b/homeassistant/components/hassio/auth.py @@ -10,8 +10,7 @@ import voluptuous as vol from homeassistant.auth.models import User from homeassistant.auth.providers import homeassistant as auth_ha -from homeassistant.components.http import HomeAssistantView -from homeassistant.components.http.const import KEY_HASS_USER +from homeassistant.components.http import KEY_HASS_USER, HomeAssistantView from homeassistant.components.http.data_validator import RequestDataValidator from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/hassio/binary_sensor.py b/homeassistant/components/hassio/binary_sensor.py index 6ddc15e7725..16845e6f76c 100644 --- a/homeassistant/components/hassio/binary_sensor.py +++ b/homeassistant/components/hassio/binary_sensor.py @@ -13,14 +13,8 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import ADDONS_COORDINATOR -from .const import ( - ATTR_STARTED, - ATTR_STATE, - ATTR_UPDATE_AVAILABLE, - DATA_KEY_ADDONS, - DATA_KEY_OS, -) -from .entity import HassioAddonEntity, HassioOSEntity +from .const import ATTR_STARTED, ATTR_STATE, DATA_KEY_ADDONS +from .entity import HassioAddonEntity @dataclass @@ -30,17 +24,7 @@ class HassioBinarySensorEntityDescription(BinarySensorEntityDescription): target: str | None = None -COMMON_ENTITY_DESCRIPTIONS = ( - HassioBinarySensorEntityDescription( - # Deprecated, scheduled to be removed in 2022.6 - device_class=BinarySensorDeviceClass.UPDATE, - entity_registry_enabled_default=False, - key=ATTR_UPDATE_AVAILABLE, - name="Update available", - ), -) - -ADDON_ENTITY_DESCRIPTIONS = COMMON_ENTITY_DESCRIPTIONS + ( +ADDON_ENTITY_DESCRIPTIONS = ( HassioBinarySensorEntityDescription( device_class=BinarySensorDeviceClass.RUNNING, entity_registry_enabled_default=False, @@ -59,28 +43,15 @@ async def async_setup_entry( """Binary sensor set up for Hass.io config entry.""" coordinator = hass.data[ADDONS_COORDINATOR] - entities: list[HassioAddonBinarySensor | HassioOSBinarySensor] = [] - - for entity_description in ADDON_ENTITY_DESCRIPTIONS: - for addon in coordinator.data[DATA_KEY_ADDONS].values(): - entities.append( - HassioAddonBinarySensor( - addon=addon, - coordinator=coordinator, - entity_description=entity_description, - ) - ) - - if coordinator.is_hass_os: - for entity_description in COMMON_ENTITY_DESCRIPTIONS: - entities.append( - HassioOSBinarySensor( - coordinator=coordinator, - entity_description=entity_description, - ) - ) - - async_add_entities(entities) + async_add_entities( + HassioAddonBinarySensor( + addon=addon, + coordinator=coordinator, + entity_description=entity_description, + ) + for addon in coordinator.data[DATA_KEY_ADDONS].values() + for entity_description in ADDON_ENTITY_DESCRIPTIONS + ) class HassioAddonBinarySensor(HassioAddonEntity, BinarySensorEntity): @@ -97,12 +68,3 @@ class HassioAddonBinarySensor(HassioAddonEntity, BinarySensorEntity): if self.entity_description.target is None: return value return value == self.entity_description.target - - -class HassioOSBinarySensor(HassioOSEntity, BinarySensorEntity): - """Binary sensor to track whether an update is available for Hass.io OS.""" - - @property - def is_on(self) -> bool: - """Return true if the binary sensor is on.""" - return self.coordinator.data[DATA_KEY_OS][self.entity_description.key] diff --git a/homeassistant/components/hassio/const.py b/homeassistant/components/hassio/const.py index e4991e5fc03..e37a31ddbd6 100644 --- a/homeassistant/components/hassio/const.py +++ b/homeassistant/components/hassio/const.py @@ -42,7 +42,6 @@ EVENT_SUPERVISOR_EVENT = "supervisor_event" ATTR_AUTO_UPDATE = "auto_update" ATTR_VERSION = "version" ATTR_VERSION_LATEST = "version_latest" -ATTR_UPDATE_AVAILABLE = "update_available" ATTR_CPU_PERCENT = "cpu_percent" ATTR_CHANGELOG = "changelog" ATTR_MEMORY_PERCENT = "memory_percent" diff --git a/homeassistant/components/hassio/manifest.json b/homeassistant/components/hassio/manifest.json index 5de80fdbd19..b087eb25807 100644 --- a/homeassistant/components/hassio/manifest.json +++ b/homeassistant/components/hassio/manifest.json @@ -6,5 +6,6 @@ "after_dependencies": ["panel_custom"], "codeowners": ["@home-assistant/supervisor"], "iot_class": "local_polling", - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "system" } diff --git a/homeassistant/components/hassio/translations/bg.json b/homeassistant/components/hassio/translations/bg.json index a5581901d78..68fcf5f343e 100644 --- a/homeassistant/components/hassio/translations/bg.json +++ b/homeassistant/components/hassio/translations/bg.json @@ -5,6 +5,7 @@ "disk_total": "\u0414\u0438\u0441\u043a \u043e\u0431\u0449\u043e", "disk_used": "\u0418\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u043d \u0434\u0438\u0441\u043a", "docker_version": "\u0412\u0435\u0440\u0441\u0438\u044f \u043d\u0430 Docker", + "host_os": "\u041e\u043f\u0435\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u0430 \u0441\u0438\u0441\u0442\u0435\u043c\u0430 \u043d\u0430 \u0445\u043e\u0441\u0442\u0430", "installed_addons": "\u0418\u043d\u0441\u0442\u0430\u043b\u0438\u0440\u0430\u043d\u0438 \u0434\u043e\u0431\u0430\u0432\u043a\u0438", "supervisor_version": "\u0412\u0435\u0440\u0441\u0438\u044f \u043d\u0430 Supervisor", "update_channel": "\u041a\u0430\u043d\u0430\u043b \u0437\u0430 \u0430\u043a\u0442\u0443\u0430\u043b\u0438\u0437\u0438\u0440\u0430\u043d\u0435" diff --git a/homeassistant/components/hassio/update.py b/homeassistant/components/hassio/update.py index e68dbece5b6..dcb2b18cdd3 100644 --- a/homeassistant/components/hassio/update.py +++ b/homeassistant/components/hassio/update.py @@ -219,7 +219,6 @@ class SupervisorOSUpdateEntity(HassioOSEntity, UpdateEntity): class SupervisorSupervisorUpdateEntity(HassioSupervisorEntity, UpdateEntity): """Update entity to handle updates for the Home Assistant Supervisor.""" - _attr_auto_update = True _attr_supported_features = UpdateEntityFeature.INSTALL _attr_title = "Home Assistant Supervisor" @@ -233,6 +232,11 @@ class SupervisorSupervisorUpdateEntity(HassioSupervisorEntity, UpdateEntity): """Return native value of entity.""" return self.coordinator.data[DATA_KEY_SUPERVISOR][ATTR_VERSION] + @property + def auto_update(self) -> bool: + """Return true if auto-update is enabled for supervisor.""" + return self.coordinator.data[DATA_KEY_SUPERVISOR][ATTR_AUTO_UPDATE] + @property def release_url(self) -> str | None: """URL to the full release notes of the latest version available.""" diff --git a/homeassistant/components/hdmi_cec/__init__.py b/homeassistant/components/hdmi_cec/__init__.py index fa64b3dac41..05d5ab57841 100644 --- a/homeassistant/components/hdmi_cec/__init__.py +++ b/homeassistant/components/hdmi_cec/__init__.py @@ -17,11 +17,6 @@ from pycec.const import ( KEY_MUTE_TOGGLE, KEY_VOLUME_DOWN, KEY_VOLUME_UP, - POWER_OFF, - POWER_ON, - STATUS_PLAY, - STATUS_STILL, - STATUS_STOP, ) from pycec.network import HDMINetwork, PhysicalAddress from pycec.tcp import TcpAdapter @@ -35,12 +30,6 @@ from homeassistant.const import ( CONF_PLATFORM, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, - STATE_IDLE, - STATE_OFF, - STATE_ON, - STATE_PAUSED, - STATE_PLAYING, - STATE_UNAVAILABLE, ) from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.helpers import discovery, event @@ -382,7 +371,6 @@ class CecEntity(Entity): def __init__(self, device, logical) -> None: """Initialize the device.""" self._device = device - self._state: str | None = None self._logical_address = logical self.entity_id = "%s.%d" % (DOMAIN, self._logical_address) self._set_attr_name() @@ -405,27 +393,9 @@ class CecEntity(Entity): self._attr_name = f"{self._device.type_name} {self._logical_address} ({self._device.osd_name})" def _hdmi_cec_unavailable(self, callback_event): - # Change state to unavailable. Without this, entity would remain in - # its last state, since the state changes are pushed. - self._state = STATE_UNAVAILABLE + self._attr_available = False self.schedule_update_ha_state(False) - def update(self): - """Update device status.""" - device = self._device - if device.power_status in [POWER_OFF, 3]: - self._state = STATE_OFF - elif device.status == STATUS_PLAY: - self._state = STATE_PLAYING - elif device.status == STATUS_STOP: - self._state = STATE_IDLE - elif device.status == STATUS_STILL: - self._state = STATE_PAUSED - elif device.power_status in [POWER_ON, 4]: - self._state = STATE_ON - else: - _LOGGER.warning("Unknown state: %d", device.power_status) - async def async_added_to_hass(self): """Register HDMI callbacks after initialization.""" self._device.set_update_callback(self._update) @@ -435,6 +405,7 @@ class CecEntity(Entity): def _update(self, device=None): """Device status changed, schedule an update.""" + self._attr_available = True self.schedule_update_ha_state(True) @property diff --git a/homeassistant/components/hdmi_cec/media_player.py b/homeassistant/components/hdmi_cec/media_player.py index e3ec00749c1..cfe73ff7c40 100644 --- a/homeassistant/components/hdmi_cec/media_player.py +++ b/homeassistant/components/hdmi_cec/media_player.py @@ -26,16 +26,10 @@ from pycec.const import ( ) from homeassistant.components.media_player import ( + DOMAIN as MP_DOMAIN, MediaPlayerEntity, MediaPlayerEntityFeature, -) -from homeassistant.components.media_player.const import DOMAIN as MP_DOMAIN -from homeassistant.const import ( - STATE_IDLE, - STATE_OFF, - STATE_ON, - STATE_PAUSED, - STATE_PLAYING, + MediaPlayerState, ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -95,7 +89,7 @@ class CecPlayerEntity(CecEntity, MediaPlayerEntity): def turn_on(self) -> None: """Turn device on.""" self._device.turn_on() - self._state = STATE_ON + self._attr_state = MediaPlayerState.ON def clear_playlist(self) -> None: """Clear players playlist.""" @@ -104,12 +98,12 @@ class CecPlayerEntity(CecEntity, MediaPlayerEntity): def turn_off(self) -> None: """Turn device off.""" self._device.turn_off() - self._state = STATE_OFF + self._attr_state = MediaPlayerState.OFF def media_stop(self) -> None: """Stop playback.""" self.send_keypress(KEY_STOP) - self._state = STATE_IDLE + self._attr_state = MediaPlayerState.IDLE def play_media(self, media_type: str, media_id: str, **kwargs: Any) -> None: """Not supported.""" @@ -130,7 +124,7 @@ class CecPlayerEntity(CecEntity, MediaPlayerEntity): def media_pause(self) -> None: """Pause playback.""" self.send_keypress(KEY_PAUSE) - self._state = STATE_PAUSED + self._attr_state = MediaPlayerState.PAUSED def select_source(self, source: str) -> None: """Not supported.""" @@ -139,7 +133,7 @@ class CecPlayerEntity(CecEntity, MediaPlayerEntity): def media_play(self) -> None: """Start playback.""" self.send_keypress(KEY_PLAY) - self._state = STATE_PLAYING + self._attr_state = MediaPlayerState.PLAYING def volume_up(self) -> None: """Increase volume.""" @@ -151,25 +145,20 @@ class CecPlayerEntity(CecEntity, MediaPlayerEntity): _LOGGER.debug("%s: volume down", self._logical_address) self.send_keypress(KEY_VOLUME_DOWN) - @property - def state(self) -> str | None: - """Cache state of device.""" - return self._state - def update(self) -> None: """Update device status.""" device = self._device if device.power_status in [POWER_OFF, 3]: - self._state = STATE_OFF + self._attr_state = MediaPlayerState.OFF elif not self.support_pause: if device.power_status in [POWER_ON, 4]: - self._state = STATE_ON + self._attr_state = MediaPlayerState.ON elif device.status == STATUS_PLAY: - self._state = STATE_PLAYING + self._attr_state = MediaPlayerState.PLAYING elif device.status == STATUS_STOP: - self._state = STATE_IDLE + self._attr_state = MediaPlayerState.IDLE elif device.status == STATUS_STILL: - self._state = STATE_PAUSED + self._attr_state = MediaPlayerState.PAUSED else: _LOGGER.warning("Unknown state: %s", device.status) diff --git a/homeassistant/components/hdmi_cec/switch.py b/homeassistant/components/hdmi_cec/switch.py index b44e5ce5c64..a554594a219 100644 --- a/homeassistant/components/hdmi_cec/switch.py +++ b/homeassistant/components/hdmi_cec/switch.py @@ -4,8 +4,9 @@ from __future__ import annotations import logging from typing import Any +from pycec.const import POWER_OFF, POWER_ON + from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN, SwitchEntity -from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -44,16 +45,21 @@ class CecSwitchEntity(CecEntity, SwitchEntity): def turn_on(self, **kwargs: Any) -> None: """Turn device on.""" self._device.turn_on() - self._state = STATE_ON + self._attr_is_on = True self.schedule_update_ha_state(force_refresh=False) def turn_off(self, **kwargs: Any) -> None: """Turn device off.""" self._device.turn_off() - self._state = STATE_OFF + self._attr_is_on = False self.schedule_update_ha_state(force_refresh=False) - @property - def is_on(self) -> bool: - """Return True if entity is on.""" - return self._state == STATE_ON + def update(self) -> None: + """Update device status.""" + device = self._device + if device.power_status in {POWER_OFF, 3}: + self._attr_is_on = False + elif device.power_status in {POWER_ON, 4}: + self._attr_is_on = True + else: + _LOGGER.warning("Unknown state: %d", device.power_status) diff --git a/homeassistant/components/heatmiser/climate.py b/homeassistant/components/heatmiser/climate.py index 942bb673cf9..7f6bd0ccf9c 100644 --- a/homeassistant/components/heatmiser/climate.py +++ b/homeassistant/components/heatmiser/climate.py @@ -2,12 +2,17 @@ from __future__ import annotations import logging +from typing import Any from heatmiserV3 import connection, heatmiser import voluptuous as vol -from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateEntity -from homeassistant.components.climate.const import ClimateEntityFeature, HVACMode +from homeassistant.components.climate import ( + PLATFORM_SCHEMA, + ClimateEntity, + ClimateEntityFeature, + HVACMode, +) from homeassistant.const import ( ATTR_TEMPERATURE, CONF_HOST, @@ -84,18 +89,12 @@ class HeatmiserV3Thermostat(ClimateEntity): self._id = device self.dcb = None self._attr_hvac_mode = HVACMode.HEAT - self._temperature_unit = None @property def name(self): """Return the name of the thermostat, if any.""" return self._name - @property - def temperature_unit(self): - """Return the unit of measurement which this thermostat uses.""" - return self._temperature_unit - @property def current_temperature(self): """Return the current temperature.""" @@ -106,9 +105,10 @@ class HeatmiserV3Thermostat(ClimateEntity): """Return the temperature we try to reach.""" return self._target_temperature - def set_temperature(self, **kwargs): + def set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" - temperature = kwargs.get(ATTR_TEMPERATURE) + if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None: + return self._target_temperature = int(temperature) self.therm.set_target_temp(self._target_temperature) @@ -119,7 +119,7 @@ class HeatmiserV3Thermostat(ClimateEntity): _LOGGER.error("Failed to update device %s", self._name) return self.dcb = self.therm.read_dcb() - self._temperature_unit = ( + self._attr_temperature_unit = ( TEMP_CELSIUS if (self.therm.get_temperature_format() == "C") else TEMP_FAHRENHEIT diff --git a/homeassistant/components/heos/media_player.py b/homeassistant/components/heos/media_player.py index b72e3ec73c1..0a5bcdc4d0a 100644 --- a/homeassistant/components/heos/media_player.py +++ b/homeassistant/components/heos/media_player.py @@ -12,23 +12,17 @@ from typing_extensions import ParamSpec from homeassistant.components import media_source from homeassistant.components.media_player import ( + ATTR_MEDIA_ENQUEUE, + DOMAIN, + BrowseMedia, MediaPlayerEnqueue, MediaPlayerEntity, MediaPlayerEntityFeature, -) -from homeassistant.components.media_player.browse_media import ( - BrowseMedia, + MediaPlayerState, + MediaType, async_process_play_media_url, ) -from homeassistant.components.media_player.const import ( - ATTR_MEDIA_ENQUEUE, - DOMAIN, - MEDIA_TYPE_MUSIC, - MEDIA_TYPE_PLAYLIST, - MEDIA_TYPE_URL, -) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import STATE_IDLE, STATE_PAUSED, STATE_PLAYING from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, @@ -62,9 +56,9 @@ BASE_SUPPORTED_FEATURES = ( ) PLAY_STATE_TO_STATE = { - heos_const.PLAY_STATE_PLAY: STATE_PLAYING, - heos_const.PLAY_STATE_STOP: STATE_IDLE, - heos_const.PLAY_STATE_PAUSE: STATE_PAUSED, + heos_const.PLAY_STATE_PLAY: MediaPlayerState.PLAYING, + heos_const.PLAY_STATE_STOP: MediaPlayerState.IDLE, + heos_const.PLAY_STATE_PAUSE: MediaPlayerState.PAUSED, } CONTROL_TO_SUPPORT = { @@ -118,6 +112,7 @@ def log_command_error( class HeosMediaPlayer(MediaPlayerEntity): """The HEOS player.""" + _attr_media_content_type = MediaType.MUSIC _attr_should_poll = False def __init__(self, player): @@ -205,13 +200,13 @@ class HeosMediaPlayer(MediaPlayerEntity): ) -> None: """Play a piece of media.""" if media_source.is_media_source_id(media_id): - media_type = MEDIA_TYPE_URL + media_type = MediaType.URL play_item = await media_source.async_resolve_media( self.hass, media_id, self.entity_id ) media_id = play_item.url - if media_type in (MEDIA_TYPE_URL, MEDIA_TYPE_MUSIC): + if media_type in {MediaType.URL, MediaType.MUSIC}: media_id = async_process_play_media_url(self.hass, media_id) await self._player.play_url(media_id) @@ -233,7 +228,7 @@ class HeosMediaPlayer(MediaPlayerEntity): await self._player.play_quick_select(index) return - if media_type == MEDIA_TYPE_PLAYLIST: + if media_type == MediaType.PLAYLIST: playlists = await self._player.heos.get_playlists() playlist = next((p for p in playlists if p.name == media_id), None) if not playlist: @@ -356,11 +351,6 @@ class HeosMediaPlayer(MediaPlayerEntity): """Content ID of current playing media.""" return self._player.now_playing_media.media_id - @property - def media_content_type(self) -> str: - """Content type of current playing media.""" - return MEDIA_TYPE_MUSIC - @property def media_duration(self): """Duration of current playing media in seconds.""" diff --git a/homeassistant/components/here_travel_time/config_flow.py b/homeassistant/components/here_travel_time/config_flow.py index d42e6d6bf3e..09faf95177d 100644 --- a/homeassistant/components/here_travel_time/config_flow.py +++ b/homeassistant/components/here_travel_time/config_flow.py @@ -10,7 +10,6 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import ( CONF_API_KEY, - CONF_ENTITY_NAMESPACE, CONF_LATITUDE, CONF_LONGITUDE, CONF_MODE, @@ -27,19 +26,22 @@ from homeassistant.helpers.selector import ( ) from .const import ( - CONF_ARRIVAL, CONF_ARRIVAL_TIME, - CONF_DEPARTURE, CONF_DEPARTURE_TIME, CONF_DESTINATION, + CONF_DESTINATION_ENTITY_ID, + CONF_DESTINATION_LATITUDE, + CONF_DESTINATION_LONGITUDE, CONF_ORIGIN, + CONF_ORIGIN_ENTITY_ID, + CONF_ORIGIN_LATITUDE, + CONF_ORIGIN_LONGITUDE, CONF_ROUTE_MODE, CONF_TRAFFIC_MODE, DEFAULT_NAME, DOMAIN, ROUTE_MODE_FASTEST, ROUTE_MODES, - TRAFFIC_MODE_DISABLED, TRAFFIC_MODE_ENABLED, TRAFFIC_MODES, TRAVEL_MODE_CAR, @@ -47,57 +49,10 @@ from .const import ( TRAVEL_MODES, UNITS, ) -from .sensor import ( - CONF_DESTINATION_ENTITY_ID, - CONF_DESTINATION_LATITUDE, - CONF_DESTINATION_LONGITUDE, - CONF_ORIGIN_ENTITY_ID, - CONF_ORIGIN_LATITUDE, - CONF_ORIGIN_LONGITUDE, -) _LOGGER = logging.getLogger(__name__) -def is_dupe_import( - entry: config_entries.ConfigEntry, - user_input: dict[str, Any], - options: dict[str, Any], -) -> bool: - """Return whether imported config already exists.""" - # Check the main data keys - if any( - user_input[key] != entry.data[key] - for key in (CONF_API_KEY, CONF_MODE, CONF_NAME) - ): - return False - - # Check origin/destination - for key in ( - CONF_DESTINATION_LATITUDE, - CONF_DESTINATION_LONGITUDE, - CONF_ORIGIN_LATITUDE, - CONF_ORIGIN_LONGITUDE, - CONF_DESTINATION_ENTITY_ID, - CONF_ORIGIN_ENTITY_ID, - ): - if user_input.get(key) != entry.data.get(key): - return False - - # We have to check for options that don't have defaults - for key in ( - CONF_TRAFFIC_MODE, - CONF_UNIT_SYSTEM, - CONF_ROUTE_MODE, - CONF_ARRIVAL_TIME, - CONF_DEPARTURE_TIME, - ): - if options.get(key) != entry.options.get(key): - return False - - return True - - def validate_api_key(api_key: str) -> None: """Validate the user input allows us to connect.""" known_working_origin = [38.9, -77.04833] @@ -275,66 +230,6 @@ class HERETravelTimeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) return self.async_show_form(step_id="destination_entity", data_schema=schema) - async def async_step_import(self, user_input: dict[str, Any]) -> FlowResult: - """Import from configuration.yaml.""" - options: dict[str, Any] = {} - user_input, options = self._transform_import_input(user_input) - # We need to prevent duplicate imports - if any( - is_dupe_import(entry, user_input, options) - for entry in self.hass.config_entries.async_entries(DOMAIN) - if entry.source == config_entries.SOURCE_IMPORT - ): - return self.async_abort(reason="already_configured") - return self.async_create_entry( - title=user_input[CONF_NAME], data=user_input, options=options - ) - - def _transform_import_input( - self, import_input: dict[str, Any] - ) -> tuple[dict[str, Any], dict[str, Any]]: - """Transform platform schema input to new model.""" - options: dict[str, Any] = {} - user_input: dict[str, Any] = {} - - if import_input.get(CONF_ORIGIN_LATITUDE) is not None: - user_input[CONF_ORIGIN_LATITUDE] = import_input[CONF_ORIGIN_LATITUDE] - user_input[CONF_ORIGIN_LONGITUDE] = import_input[CONF_ORIGIN_LONGITUDE] - else: - user_input[CONF_ORIGIN_ENTITY_ID] = import_input[CONF_ORIGIN_ENTITY_ID] - - if import_input.get(CONF_DESTINATION_LATITUDE) is not None: - user_input[CONF_DESTINATION_LATITUDE] = import_input[ - CONF_DESTINATION_LATITUDE - ] - user_input[CONF_DESTINATION_LONGITUDE] = import_input[ - CONF_DESTINATION_LONGITUDE - ] - else: - user_input[CONF_DESTINATION_ENTITY_ID] = import_input[ - CONF_DESTINATION_ENTITY_ID - ] - - user_input[CONF_API_KEY] = import_input[CONF_API_KEY] - user_input[CONF_MODE] = import_input[CONF_MODE] - user_input[CONF_NAME] = import_input[CONF_NAME] - if (namespace := import_input.get(CONF_ENTITY_NAMESPACE)) is not None: - user_input[CONF_NAME] = f"{namespace} {user_input[CONF_NAME]}" - - options[CONF_TRAFFIC_MODE] = ( - TRAFFIC_MODE_ENABLED - if import_input.get(CONF_TRAFFIC_MODE, False) - else TRAFFIC_MODE_DISABLED - ) - options[CONF_ROUTE_MODE] = import_input.get(CONF_ROUTE_MODE) - options[CONF_UNIT_SYSTEM] = import_input.get( - CONF_UNIT_SYSTEM, self.hass.config.units.name - ) - options[CONF_ARRIVAL_TIME] = import_input.get(CONF_ARRIVAL, None) - options[CONF_DEPARTURE_TIME] = import_input.get(CONF_DEPARTURE, None) - - return user_input, options - class HERETravelTimeOptionsFlow(config_entries.OptionsFlow): """Handle HERE Travel Time options.""" diff --git a/homeassistant/components/here_travel_time/sensor.py b/homeassistant/components/here_travel_time/sensor.py index e7bc88f2210..74a9ae357e1 100644 --- a/homeassistant/components/here_travel_time/sensor.py +++ b/homeassistant/components/here_travel_time/sensor.py @@ -3,35 +3,30 @@ from __future__ import annotations from collections.abc import Mapping from datetime import timedelta -import logging from typing import Any -import voluptuous as vol - from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, SensorEntity, SensorEntityDescription, SensorStateClass, ) -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ATTRIBUTION, ATTR_LATITUDE, ATTR_LONGITUDE, - CONF_API_KEY, CONF_MODE, CONF_NAME, - CONF_UNIT_SYSTEM, + CONF_UNIT_SYSTEM_IMPERIAL, + LENGTH_KILOMETERS, + LENGTH_MILES, TIME_MINUTES, ) 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.start import async_at_start -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import HereTravelTimeDataUpdateCoordinator @@ -44,83 +39,13 @@ from .const import ( ATTR_ORIGIN, ATTR_ORIGIN_NAME, ATTR_ROUTE, - CONF_ARRIVAL, - CONF_DEPARTURE, - CONF_DESTINATION_ENTITY_ID, - CONF_DESTINATION_LATITUDE, - CONF_DESTINATION_LONGITUDE, - CONF_ORIGIN_ENTITY_ID, - CONF_ORIGIN_LATITUDE, - CONF_ORIGIN_LONGITUDE, - CONF_ROUTE_MODE, - CONF_TRAFFIC_MODE, - DEFAULT_NAME, DOMAIN, ICON_CAR, ICONS, - ROUTE_MODE_FASTEST, - ROUTE_MODES, - TRAVEL_MODE_BICYCLE, - TRAVEL_MODE_CAR, - TRAVEL_MODE_PEDESTRIAN, - TRAVEL_MODE_PUBLIC, - TRAVEL_MODE_PUBLIC_TIME_TABLE, - TRAVEL_MODE_TRUCK, - TRAVEL_MODES, - UNITS, ) -_LOGGER = logging.getLogger(__name__) - - SCAN_INTERVAL = timedelta(minutes=5) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_API_KEY): cv.string, - vol.Inclusive( - CONF_DESTINATION_LATITUDE, "destination_coordinates" - ): cv.latitude, - vol.Inclusive( - CONF_DESTINATION_LONGITUDE, "destination_coordinates" - ): cv.longitude, - vol.Exclusive(CONF_DESTINATION_LATITUDE, "destination"): cv.latitude, - vol.Exclusive(CONF_DESTINATION_ENTITY_ID, "destination"): cv.entity_id, - vol.Inclusive(CONF_ORIGIN_LATITUDE, "origin_coordinates"): cv.latitude, - vol.Inclusive(CONF_ORIGIN_LONGITUDE, "origin_coordinates"): cv.longitude, - vol.Exclusive(CONF_ORIGIN_LATITUDE, "origin"): cv.latitude, - vol.Exclusive(CONF_ORIGIN_ENTITY_ID, "origin"): cv.entity_id, - vol.Optional(CONF_DEPARTURE): cv.time, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_MODE, default=TRAVEL_MODE_CAR): vol.In(TRAVEL_MODES), - vol.Optional(CONF_ROUTE_MODE, default=ROUTE_MODE_FASTEST): vol.In(ROUTE_MODES), - vol.Optional(CONF_TRAFFIC_MODE, default=False): cv.boolean, - vol.Optional(CONF_UNIT_SYSTEM): vol.In(UNITS), - } -) - -PLATFORM_SCHEMA = vol.All( - cv.has_at_least_one_key(CONF_DESTINATION_LATITUDE, CONF_DESTINATION_ENTITY_ID), - cv.has_at_least_one_key(CONF_ORIGIN_LATITUDE, CONF_ORIGIN_ENTITY_ID), - cv.key_value_schemas( - CONF_MODE, - { - None: PLATFORM_SCHEMA, - TRAVEL_MODE_BICYCLE: PLATFORM_SCHEMA, - TRAVEL_MODE_CAR: PLATFORM_SCHEMA, - TRAVEL_MODE_PEDESTRIAN: PLATFORM_SCHEMA, - TRAVEL_MODE_PUBLIC: PLATFORM_SCHEMA, - TRAVEL_MODE_TRUCK: PLATFORM_SCHEMA, - TRAVEL_MODE_PUBLIC_TIME_TABLE: PLATFORM_SCHEMA.extend( - { - vol.Exclusive(CONF_ARRIVAL, "arrival_departure"): cv.time, - vol.Exclusive(CONF_DEPARTURE, "arrival_departure"): cv.time, - } - ), - }, - ), -) - def sensor_descriptions(travel_mode: str) -> tuple[SensorEntityDescription, ...]: """Construct SensorEntityDescriptions.""" @@ -139,12 +64,6 @@ def sensor_descriptions(travel_mode: str) -> tuple[SensorEntityDescription, ...] state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=TIME_MINUTES, ), - SensorEntityDescription( - name="Distance", - icon=ICONS.get(travel_mode, ICON_CAR), - key=ATTR_DISTANCE, - state_class=SensorStateClass.MEASUREMENT, - ), SensorEntityDescription( name="Route", icon="mdi:directions", @@ -153,28 +72,6 @@ def sensor_descriptions(travel_mode: str) -> tuple[SensorEntityDescription, ...] ) -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the HERE travel time platform.""" - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data=config, - ) - ) - - _LOGGER.warning( - "Your HERE travel time configuration has been imported into the UI; " - "please remove it from configuration.yaml as support for it will be " - "removed in a future release" - ) - - async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, @@ -198,6 +95,7 @@ async def async_setup_entry( ) sensors.append(OriginSensor(entry_id, name, coordinator)) sensors.append(DestinationSensor(entry_id, name, coordinator)) + sensors.append(DistanceSensor(entry_id, name, coordinator)) async_add_entities(sensors) @@ -301,3 +199,29 @@ class DestinationSensor(HERETravelTimeSensor): ATTR_LONGITUDE: self.coordinator.data[ATTR_DESTINATION].split(",")[1], } return None + + +class DistanceSensor(HERETravelTimeSensor): + """Sensor holding information about the distance.""" + + def __init__( + self, + unique_id_prefix: str, + name: str, + coordinator: HereTravelTimeDataUpdateCoordinator, + ) -> None: + """Initialize the sensor.""" + sensor_description = SensorEntityDescription( + name="Distance", + icon=ICONS.get(coordinator.config.travel_mode, ICON_CAR), + key=ATTR_DISTANCE, + state_class=SensorStateClass.MEASUREMENT, + ) + super().__init__(unique_id_prefix, name, sensor_description, coordinator) + + @property + def native_unit_of_measurement(self) -> str | None: + """Return the unit of measurement of the sensor.""" + if self.coordinator.config.units == CONF_UNIT_SYSTEM_IMPERIAL: + return LENGTH_MILES + return LENGTH_KILOMETERS diff --git a/homeassistant/components/here_travel_time/translations/bg.json b/homeassistant/components/here_travel_time/translations/bg.json index 75bb03c2a1f..8202e295a74 100644 --- a/homeassistant/components/here_travel_time/translations/bg.json +++ b/homeassistant/components/here_travel_time/translations/bg.json @@ -14,12 +14,38 @@ "destination_entity_id": { "title": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u0434\u0435\u0441\u0442\u0438\u043d\u0430\u0446\u0438\u044f" }, + "destination_menu": { + "title": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u0434\u0435\u0441\u0442\u0438\u043d\u0430\u0446\u0438\u044f" + }, "user": { "data": { "api_key": "API \u043a\u043b\u044e\u0447", + "mode": "\u0420\u0435\u0436\u0438\u043c \u043d\u0430 \u043f\u044a\u0442\u0443\u0432\u0430\u043d\u0435", "name": "\u0418\u043c\u0435" } } } + }, + "options": { + "step": { + "arrival_time": { + "data": { + "arrival_time": "\u0427\u0430\u0441 \u043d\u0430 \u043f\u0440\u0438\u0441\u0442\u0438\u0433\u0430\u043d\u0435" + }, + "title": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u0447\u0430\u0441 \u043d\u0430 \u043f\u0440\u0438\u0441\u0442\u0438\u0433\u0430\u043d\u0435" + }, + "departure_time": { + "data": { + "departure_time": "\u0427\u0430\u0441 \u043d\u0430 \u0437\u0430\u043c\u0438\u043d\u0430\u0432\u0430\u043d\u0435" + }, + "title": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u0447\u0430\u0441 \u043d\u0430 \u0437\u0430\u043c\u0438\u043d\u0430\u0432\u0430\u043d\u0435" + }, + "init": { + "data": { + "route_mode": "\u0420\u0435\u0436\u0438\u043c \u043d\u0430 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0430", + "traffic_mode": "\u0420\u0435\u0436\u0438\u043c \u043d\u0430 \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u0435" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/here_travel_time/translations/cs.json b/homeassistant/components/here_travel_time/translations/cs.json new file mode 100644 index 00000000000..d0cbf3d50c3 --- /dev/null +++ b/homeassistant/components/here_travel_time/translations/cs.json @@ -0,0 +1,11 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + }, + "error": { + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/here_travel_time/translations/ja.json b/homeassistant/components/here_travel_time/translations/ja.json index ae3eb1d4f73..5d83a13fbe0 100644 --- a/homeassistant/components/here_travel_time/translations/ja.json +++ b/homeassistant/components/here_travel_time/translations/ja.json @@ -10,7 +10,7 @@ "step": { "destination_coordinates": { "data": { - "destination": "GPS\u5ea7\u6a19\u3068\u3057\u3066\u306e\u76ee\u7684\u5730" + "destination": "GPS \u5ea7\u6a19\u3068\u3057\u3066\u306e\u76ee\u7684\u5730" }, "title": "\u76ee\u7684\u5730\u3092\u9078\u629e" }, diff --git a/homeassistant/components/hisense_aehw4a1/climate.py b/homeassistant/components/hisense_aehw4a1/climate.py index 9612fd74f0b..fc956ec8760 100644 --- a/homeassistant/components/hisense_aehw4a1/climate.py +++ b/homeassistant/components/hisense_aehw4a1/climate.py @@ -7,8 +7,7 @@ from typing import Any from pyaehw4a1.aehw4a1 import AehW4a1 import pyaehw4a1.exceptions -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( FAN_AUTO, FAN_HIGH, FAN_LOW, @@ -21,6 +20,7 @@ from homeassistant.components.climate.const import ( SWING_HORIZONTAL, SWING_OFF, SWING_VERTICAL, + ClimateEntity, ClimateEntityFeature, HVACMode, ) @@ -158,9 +158,8 @@ class ClimateAehW4a1(ClimateEntity): self._fan_modes = FAN_MODES self._swing_modes = SWING_MODES self._preset_modes = PRESET_MODES - self._available = None + self._attr_available = False self._on = None - self._temperature_unit = None self._current_temperature = None self._target_temperature = None self._attr_hvac_mode = None @@ -177,17 +176,17 @@ class ClimateAehW4a1(ClimateEntity): _LOGGER.warning( "Unexpected error of %s: %s", self._unique_id, library_error ) - self._available = False + self._attr_available = False return - self._available = True + self._attr_available = True self._on = status["run_status"] if status["temperature_Fahrenheit"] == "0": - self._temperature_unit = TEMP_CELSIUS + self._attr_temperature_unit = TEMP_CELSIUS else: - self._temperature_unit = TEMP_FAHRENHEIT + self._attr_temperature_unit = TEMP_FAHRENHEIT self._current_temperature = int(status["indoor_temperature_status"], 2) @@ -227,21 +226,11 @@ class ClimateAehW4a1(ClimateEntity): self._target_temperature = None self._preset_mode = None - @property - def available(self): - """Return True if entity is available.""" - return self._available - @property def name(self): """Return the name of the climate device.""" return self._unique_id - @property - def temperature_unit(self): - """Return the unit of measurement.""" - return self._temperature_unit - @property def current_temperature(self): """Return the current temperature.""" @@ -285,14 +274,14 @@ class ClimateAehW4a1(ClimateEntity): @property def min_temp(self): """Return the minimum temperature.""" - if self._temperature_unit == TEMP_CELSIUS: + if self.temperature_unit == TEMP_CELSIUS: return MIN_TEMP_C return MIN_TEMP_F @property def max_temp(self): """Return the maximum temperature.""" - if self._temperature_unit == TEMP_CELSIUS: + if self.temperature_unit == TEMP_CELSIUS: return MAX_TEMP_C return MAX_TEMP_F @@ -312,7 +301,7 @@ class ClimateAehW4a1(ClimateEntity): _LOGGER.debug("Setting temp of %s to %s", self._unique_id, temp) if self._preset_mode != PRESET_NONE: await self.async_set_preset_mode(PRESET_NONE) - if self._temperature_unit == TEMP_CELSIUS: + if self.temperature_unit == TEMP_CELSIUS: await self._device.command(f"temp_{int(temp)}_C") else: await self._device.command(f"temp_{int(temp)}_F") diff --git a/homeassistant/components/history/__init__.py b/homeassistant/components/history/__init__.py index 77301532d3d..bae6c95507e 100644 --- a/homeassistant/components/history/__init__.py +++ b/homeassistant/components/history/__init__.py @@ -6,22 +6,22 @@ from datetime import datetime as dt, timedelta from http import HTTPStatus import logging import time -from typing import Literal, cast +from typing import cast from aiohttp import web import voluptuous as vol from homeassistant.components import frontend, websocket_api from homeassistant.components.http import HomeAssistantView -from homeassistant.components.recorder import get_instance, history +from homeassistant.components.recorder import ( + get_instance, + history, + websocket_api as recorder_ws, +) from homeassistant.components.recorder.filters import ( Filters, sqlalchemy_filter_from_include_exclude_conf, ) -from homeassistant.components.recorder.statistics import ( - list_statistic_ids, - statistics_during_period, -) from homeassistant.components.recorder.util import session_scope from homeassistant.components.websocket_api import messages from homeassistant.core import HomeAssistant @@ -68,23 +68,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return True -def _ws_get_statistics_during_period( - hass: HomeAssistant, - msg_id: int, - start_time: dt, - end_time: dt | None = None, - statistic_ids: list[str] | None = None, - period: Literal["5minute", "day", "hour", "month"] = "hour", -) -> str: - """Fetch statistics and convert them to json in the executor.""" - return JSON_DUMP( - messages.result_message( - msg_id, - statistics_during_period(hass, start_time, end_time, statistic_ids, period), - ) - ) - - @websocket_api.websocket_command( { vol.Required("type"): "history/statistics_during_period", @@ -99,46 +82,11 @@ async def ws_get_statistics_during_period( hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict ) -> None: """Handle statistics 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 - - connection.send_message( - await get_instance(hass).async_add_executor_job( - _ws_get_statistics_during_period, - hass, - msg["id"], - start_time, - end_time, - msg.get("statistic_ids"), - msg.get("period"), - ) - ) - - -def _ws_get_list_statistic_ids( - hass: HomeAssistant, - 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.""" - return JSON_DUMP( - messages.result_message(msg_id, list_statistic_ids(hass, None, statistic_type)) + _LOGGER.warning( + "WS API 'history/statistics_during_period' is deprecated and will be removed in " + "Home Assistant Core 2022.12. Use 'recorder/statistics_during_period' instead" ) + await recorder_ws.ws_handle_get_statistics_during_period(hass, connection, msg) @websocket_api.websocket_command( @@ -152,14 +100,11 @@ async def ws_get_list_statistic_ids( hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict ) -> None: """Fetch a list of available statistic_id.""" - connection.send_message( - await get_instance(hass).async_add_executor_job( - _ws_get_list_statistic_ids, - hass, - msg["id"], - msg.get("statistic_type"), - ) + _LOGGER.warning( + "WS API 'history/list_statistic_ids' is deprecated and will be removed in " + "Home Assistant Core 2022.12. Use 'recorder/list_statistic_ids' instead" ) + await recorder_ws.ws_handle_list_statistic_ids(hass, connection, msg) def _ws_get_significant_states( diff --git a/homeassistant/components/history/manifest.json b/homeassistant/components/history/manifest.json index 7185a8b63c4..4ebf64dd603 100644 --- a/homeassistant/components/history/manifest.json +++ b/homeassistant/components/history/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/history", "dependencies": ["http", "recorder"], "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "system" } diff --git a/homeassistant/components/hitron_coda/device_tracker.py b/homeassistant/components/hitron_coda/device_tracker.py index 749347e82fe..c9ee93634b2 100644 --- a/homeassistant/components/hitron_coda/device_tracker.py +++ b/homeassistant/components/hitron_coda/device_tracker.py @@ -32,7 +32,9 @@ PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( ) -def get_scanner(_hass: HomeAssistant, config: ConfigType) -> DeviceScanner | None: +def get_scanner( + _hass: HomeAssistant, config: ConfigType +) -> HitronCODADeviceScanner | None: """Validate the configuration and return a Hitron CODA-4582U scanner.""" scanner = HitronCODADeviceScanner(config[DOMAIN]) diff --git a/homeassistant/components/hive/climate.py b/homeassistant/components/hive/climate.py index b6f4f8270b4..620d679fe1c 100644 --- a/homeassistant/components/hive/climate.py +++ b/homeassistant/components/hive/climate.py @@ -5,10 +5,10 @@ from typing import Any import voluptuous as vol -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( PRESET_BOOST, PRESET_NONE, + ClimateEntity, ClimateEntityFeature, HVACAction, HVACMode, @@ -128,12 +128,12 @@ class HiveClimateEntity(HiveEntity, ClimateEntity): await self.hive.heating.setTargetTemperature(self.device, new_temperature) @refresh_system - async def async_set_preset_mode(self, preset_mode): + async def async_set_preset_mode(self, preset_mode: str) -> None: """Set new preset mode.""" if preset_mode == PRESET_NONE and self.preset_mode == PRESET_BOOST: await self.hive.heating.setBoostOff(self.device) elif preset_mode == PRESET_BOOST: - curtemp = round(self.current_temperature * 2) / 2 + curtemp = round((self.current_temperature or 0) * 2) / 2 temperature = curtemp + 0.5 await self.hive.heating.setBoostOn(self.device, 30, temperature) diff --git a/homeassistant/components/hive/translations/es.json b/homeassistant/components/hive/translations/es.json index 0bece2f0e54..fb419244bf6 100644 --- a/homeassistant/components/hive/translations/es.json +++ b/homeassistant/components/hive/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente", "unknown_entry": "No se puede encontrar una entrada existente." }, "error": { diff --git a/homeassistant/components/hlk_sw16/translations/cs.json b/homeassistant/components/hlk_sw16/translations/cs.json index a4bad4b7c9f..0f02cd974c2 100644 --- a/homeassistant/components/hlk_sw16/translations/cs.json +++ b/homeassistant/components/hlk_sw16/translations/cs.json @@ -4,7 +4,7 @@ "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" }, "error": { - "cannot_connect": "Nelze se p\u0159ipojit", + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, diff --git a/homeassistant/components/home_connect/binary_sensor.py b/homeassistant/components/home_connect/binary_sensor.py index c00e8303b66..93b90cbfbd3 100644 --- a/homeassistant/components/home_connect/binary_sensor.py +++ b/homeassistant/components/home_connect/binary_sensor.py @@ -68,7 +68,7 @@ class HomeConnectBinarySensor(HomeConnectEntity, BinarySensorEntity): return bool(self._state) @property - def available(self): + def available(self) -> bool: """Return true if the binary sensor is available.""" return self._state is not None diff --git a/homeassistant/components/home_connect/light.py b/homeassistant/components/home_connect/light.py index c7418060eaf..17dc842358f 100644 --- a/homeassistant/components/home_connect/light.py +++ b/homeassistant/components/home_connect/light.py @@ -93,7 +93,7 @@ class HomeConnectLight(HomeConnectEntity, LightEntity): """Return the color property.""" return self._hs_color - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Switch the light on, change brightness, change color.""" if self._ambient: _LOGGER.debug("Switching ambient light on for: %s", self.name) @@ -121,7 +121,9 @@ class HomeConnectLight(HomeConnectEntity, LightEntity): hs_color = kwargs.get(ATTR_HS_COLOR, self._hs_color) if hs_color is not None: - rgb = color_util.color_hsv_to_RGB(*hs_color, brightness) + rgb = color_util.color_hsv_to_RGB( + hs_color[0], hs_color[1], brightness + ) hex_val = color_util.color_rgb_to_hex(rgb[0], rgb[1], rgb[2]) try: await self.hass.async_add_executor_job( @@ -165,7 +167,7 @@ class HomeConnectLight(HomeConnectEntity, LightEntity): _LOGGER.error("Error while trying to turn off light: %s", err) self.async_entity_update() - async def async_update(self): + async def async_update(self) -> None: """Update the light's status.""" if self.device.appliance.status.get(self._key, {}).get(ATTR_VALUE) is True: self._state = True diff --git a/homeassistant/components/home_connect/sensor.py b/homeassistant/components/home_connect/sensor.py index de409484f1e..38a45ccf709 100644 --- a/homeassistant/components/home_connect/sensor.py +++ b/homeassistant/components/home_connect/sensor.py @@ -49,11 +49,11 @@ class HomeConnectSensor(HomeConnectEntity, SensorEntity): @property def native_value(self): - """Return true if the binary sensor is on.""" + """Return sensor value.""" return self._state @property - def available(self): + def available(self) -> bool: """Return true if the sensor is available.""" return self._state is not None diff --git a/homeassistant/components/home_connect/switch.py b/homeassistant/components/home_connect/switch.py index 2278c2b1f2d..89b1f23589f 100644 --- a/homeassistant/components/home_connect/switch.py +++ b/homeassistant/components/home_connect/switch.py @@ -60,11 +60,6 @@ class HomeConnectProgramSwitch(HomeConnectEntity, SwitchEntity): """Return true if the switch is on.""" return bool(self._state) - @property - def available(self): - """Return true if the entity is available.""" - return True - async def async_turn_on(self, **kwargs: Any) -> None: """Start the program.""" _LOGGER.debug("Tried to turn on program %s", self.program_name) diff --git a/homeassistant/components/homeassistant/logbook.py b/homeassistant/components/homeassistant/logbook.py index 548753982a8..229fb24cb27 100644 --- a/homeassistant/components/homeassistant/logbook.py +++ b/homeassistant/components/homeassistant/logbook.py @@ -3,7 +3,7 @@ from __future__ import annotations from collections.abc import Callable -from homeassistant.components.logbook.const import ( +from homeassistant.components.logbook import ( LOGBOOK_ENTRY_ICON, LOGBOOK_ENTRY_MESSAGE, LOGBOOK_ENTRY_NAME, diff --git a/homeassistant/components/homeassistant/manifest.json b/homeassistant/components/homeassistant/manifest.json index 027d1b9376d..179e8deb233 100644 --- a/homeassistant/components/homeassistant/manifest.json +++ b/homeassistant/components/homeassistant/manifest.json @@ -3,5 +3,6 @@ "name": "Home Assistant Core Integration", "documentation": "https://www.home-assistant.io/integrations/homeassistant", "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "system" } diff --git a/homeassistant/components/homeassistant/translations/bg.json b/homeassistant/components/homeassistant/translations/bg.json index dab7fd6426a..260c7bcb57c 100644 --- a/homeassistant/components/homeassistant/translations/bg.json +++ b/homeassistant/components/homeassistant/translations/bg.json @@ -2,9 +2,11 @@ "system_health": { "info": { "arch": "\u0410\u0440\u0445\u0438\u0442\u0435\u043a\u0442\u0443\u0440\u0430 \u043d\u0430 CPU", + "config_dir": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u0430 \u0434\u0438\u0440\u0435\u043a\u0442\u043e\u0440\u0438\u044f", "docker": "Docker", "hassio": "Supervisor", "installation_type": "\u0422\u0438\u043f \u0438\u043d\u0441\u0442\u0430\u043b\u0430\u0446\u0438\u044f", + "os_name": "\u0424\u0430\u043c\u0438\u043b\u0438\u044f \u043e\u043f\u0435\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u0438 \u0441\u0438\u0441\u0442\u0435\u043c\u0438", "os_version": "\u0412\u0435\u0440\u0441\u0438\u044f \u043d\u0430 \u043e\u043f\u0435\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u0430\u0442\u0430 \u0441\u0438\u0441\u0442\u0435\u043c\u0430", "python_version": "\u0412\u0435\u0440\u0441\u0438\u044f \u043d\u0430 Python", "timezone": "\u0427\u0430\u0441\u043e\u0432\u0430 \u0437\u043e\u043d\u0430", diff --git a/homeassistant/components/homeassistant/triggers/homeassistant.py b/homeassistant/components/homeassistant/triggers/homeassistant.py index 8749c47861a..e3dc93a9788 100644 --- a/homeassistant/components/homeassistant/triggers/homeassistant.py +++ b/homeassistant/components/homeassistant/triggers/homeassistant.py @@ -7,8 +7,6 @@ from homeassistant.helpers import config_validation as cv from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType -# mypy: allow-untyped-defs - EVENT_START = "start" EVENT_SHUTDOWN = "shutdown" diff --git a/homeassistant/components/homeassistant/triggers/numeric_state.py b/homeassistant/components/homeassistant/triggers/numeric_state.py index 10971a03781..b8f548adb16 100644 --- a/homeassistant/components/homeassistant/triggers/numeric_state.py +++ b/homeassistant/components/homeassistant/triggers/numeric_state.py @@ -27,9 +27,6 @@ from homeassistant.helpers.event import ( from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType -# mypy: allow-incomplete-defs, allow-untyped-calls, allow-untyped-defs -# mypy: no-check-untyped-defs - def validate_above_below(value): """Validate that above and below can co-exist.""" diff --git a/homeassistant/components/homeassistant/triggers/state.py b/homeassistant/components/homeassistant/triggers/state.py index 8514000de07..25622e0a3c6 100644 --- a/homeassistant/components/homeassistant/triggers/state.py +++ b/homeassistant/components/homeassistant/triggers/state.py @@ -29,9 +29,6 @@ from homeassistant.helpers.event import ( from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType -# mypy: allow-incomplete-defs, allow-untyped-calls, allow-untyped-defs -# mypy: no-check-untyped-defs - _LOGGER = logging.getLogger(__name__) CONF_ENTITY_ID = "entity_id" diff --git a/homeassistant/components/homeassistant/triggers/time.py b/homeassistant/components/homeassistant/triggers/time.py index a81afa1323a..a51eff004e5 100644 --- a/homeassistant/components/homeassistant/triggers/time.py +++ b/homeassistant/components/homeassistant/triggers/time.py @@ -23,8 +23,6 @@ from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType import homeassistant.util.dt as dt_util -# mypy: allow-untyped-defs, no-check-untyped-defs - _TIME_TRIGGER_SCHEMA = vol.Any( cv.time, vol.All(str, cv.entity_domain(["input_datetime", "sensor"])), diff --git a/homeassistant/components/homeassistant/triggers/time_pattern.py b/homeassistant/components/homeassistant/triggers/time_pattern.py index 3c2cf58bca8..2a5022bebf3 100644 --- a/homeassistant/components/homeassistant/triggers/time_pattern.py +++ b/homeassistant/components/homeassistant/triggers/time_pattern.py @@ -8,8 +8,6 @@ from homeassistant.helpers.event import async_track_time_change from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType -# mypy: allow-untyped-defs, no-check-untyped-defs - CONF_HOURS = "hours" CONF_MINUTES = "minutes" CONF_SECONDS = "seconds" diff --git a/homeassistant/components/homeassistant_alerts/manifest.json b/homeassistant/components/homeassistant_alerts/manifest.json index 62b729cada9..20e6447dadf 100644 --- a/homeassistant/components/homeassistant_alerts/manifest.json +++ b/homeassistant/components/homeassistant_alerts/manifest.json @@ -3,5 +3,6 @@ "name": "Home Assistant Alerts", "config_flow": false, "documentation": "https://www.home-assistant.io/integrations/homeassistant_alerts", - "codeowners": ["@home-assistant/core"] + "codeowners": ["@home-assistant/core"], + "quality_scale": "internal" } diff --git a/homeassistant/components/homeassistant_alerts/translations/bg.json b/homeassistant/components/homeassistant_alerts/translations/bg.json new file mode 100644 index 00000000000..33cafb8bba3 --- /dev/null +++ b/homeassistant/components/homeassistant_alerts/translations/bg.json @@ -0,0 +1,8 @@ +{ + "issues": { + "alert": { + "description": "{description}", + "title": "{title}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant_alerts/translations/cs.json b/homeassistant/components/homeassistant_alerts/translations/cs.json new file mode 100644 index 00000000000..33cafb8bba3 --- /dev/null +++ b/homeassistant/components/homeassistant_alerts/translations/cs.json @@ -0,0 +1,8 @@ +{ + "issues": { + "alert": { + "description": "{description}", + "title": "{title}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 6852ff4fa9f..4859d9e1065 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -23,7 +23,7 @@ from homeassistant.components.binary_sensor import ( from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN from homeassistant.components.http import HomeAssistantView from homeassistant.components.humidifier import DOMAIN as HUMIDIFIER_DOMAIN -from homeassistant.components.network.const import MDNS_TARGET_IP +from homeassistant.components.network import MDNS_TARGET_IP from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN, SensorDeviceClass from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( @@ -193,14 +193,21 @@ def _async_all_homekit_instances(hass: HomeAssistant) -> list[HomeKit]: ] -def _async_get_entries_by_name( +def _async_get_imported_entries_indices( current_entries: list[ConfigEntry], -) -> dict[str, ConfigEntry]: - """Return a dict of the entries by name.""" +) -> tuple[dict[str, ConfigEntry], dict[int, ConfigEntry]]: + """Return a dicts of the entries by name and port.""" # For backwards compat, its possible the first bridge is using the default # name. - return {entry.data.get(CONF_NAME, BRIDGE_NAME): entry for entry in current_entries} + entries_by_name: dict[str, ConfigEntry] = {} + entries_by_port: dict[int, ConfigEntry] = {} + for entry in current_entries: + if entry.source != SOURCE_IMPORT: + continue + entries_by_name[entry.data.get(CONF_NAME, BRIDGE_NAME)] = entry + entries_by_port[entry.data.get(CONF_PORT, DEFAULT_PORT)] = entry + return entries_by_name, entries_by_port async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: @@ -218,10 +225,14 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return True current_entries = hass.config_entries.async_entries(DOMAIN) - entries_by_name = _async_get_entries_by_name(current_entries) + entries_by_name, entries_by_port = _async_get_imported_entries_indices( + current_entries + ) for index, conf in enumerate(config[DOMAIN]): - if _async_update_config_entry_if_from_yaml(hass, entries_by_name, conf): + if _async_update_config_entry_from_yaml( + hass, entries_by_name, entries_by_port, conf + ): continue conf[CONF_ENTRY_INDEX] = index @@ -237,8 +248,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: @callback -def _async_update_config_entry_if_from_yaml( - hass: HomeAssistant, entries_by_name: dict[str, ConfigEntry], conf: ConfigType +def _async_update_config_entry_from_yaml( + hass: HomeAssistant, + entries_by_name: dict[str, ConfigEntry], + entries_by_port: dict[int, ConfigEntry], + conf: ConfigType, ) -> bool: """Update a config entry with the latest yaml. @@ -246,27 +260,24 @@ def _async_update_config_entry_if_from_yaml( Returns False if there is no matching config entry """ - bridge_name = conf[CONF_NAME] - - if ( - bridge_name in entries_by_name - and entries_by_name[bridge_name].source == SOURCE_IMPORT + if not ( + matching_entry := entries_by_name.get(conf.get(CONF_NAME, BRIDGE_NAME)) + or entries_by_port.get(conf.get(CONF_PORT, DEFAULT_PORT)) ): - entry = entries_by_name[bridge_name] - # If they alter the yaml config we import the changes - # since there currently is no practical way to support - # all the options in the UI at this time. - data = conf.copy() - options = {} - for key in CONFIG_OPTIONS: - if key in data: - options[key] = data[key] - del data[key] + return False - hass.config_entries.async_update_entry(entry, data=data, options=options) - return True + # If they alter the yaml config we import the changes + # since there currently is no practical way to support + # all the options in the UI at this time. + data = conf.copy() + options = {} + for key in CONFIG_OPTIONS: + if key in data: + options[key] = data[key] + del data[key] - return False + hass.config_entries.async_update_entry(matching_entry, data=data, options=options) + return True async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -451,10 +462,14 @@ def _async_register_events_and_services(hass: HomeAssistant) -> None: return current_entries = hass.config_entries.async_entries(DOMAIN) - entries_by_name = _async_get_entries_by_name(current_entries) + entries_by_name, entries_by_port = _async_get_imported_entries_indices( + current_entries + ) for conf in config[DOMAIN]: - _async_update_config_entry_if_from_yaml(hass, entries_by_name, conf) + _async_update_config_entry_from_yaml( + hass, entries_by_name, entries_by_port, conf + ) reload_tasks = [ hass.config_entries.async_reload(entry.entry_id) diff --git a/homeassistant/components/homekit/logbook.py b/homeassistant/components/homekit/logbook.py index a513b31b232..17cdac51799 100644 --- a/homeassistant/components/homekit/logbook.py +++ b/homeassistant/components/homekit/logbook.py @@ -2,7 +2,7 @@ from collections.abc import Callable from typing import Any -from homeassistant.components.logbook.const import ( +from homeassistant.components.logbook import ( LOGBOOK_ENTRY_ENTITY_ID, LOGBOOK_ENTRY_MESSAGE, LOGBOOK_ENTRY_NAME, diff --git a/homeassistant/components/homekit/type_humidifiers.py b/homeassistant/components/homekit/type_humidifiers.py index 09cfc02dcce..0babb285bed 100644 --- a/homeassistant/components/homekit/type_humidifiers.py +++ b/homeassistant/components/homekit/type_humidifiers.py @@ -3,8 +3,7 @@ import logging from pyhap.const import CATEGORY_HUMIDIFIER -from homeassistant.components.humidifier import HumidifierDeviceClass -from homeassistant.components.humidifier.const import ( +from homeassistant.components.humidifier import ( ATTR_HUMIDITY, ATTR_MAX_HUMIDITY, ATTR_MIN_HUMIDITY, @@ -12,6 +11,7 @@ from homeassistant.components.humidifier.const import ( DEFAULT_MIN_HUMIDITY, DOMAIN, SERVICE_SET_HUMIDITY, + HumidifierDeviceClass, ) from homeassistant.const import ( ATTR_DEVICE_CLASS, diff --git a/homeassistant/components/homekit/type_thermostats.py b/homeassistant/components/homekit/type_thermostats.py index fc27a85850f..a924548816b 100644 --- a/homeassistant/components/homekit/type_thermostats.py +++ b/homeassistant/components/homekit/type_thermostats.py @@ -3,8 +3,7 @@ import logging from pyhap.const import CATEGORY_THERMOSTAT -from homeassistant.components.climate import ClimateEntityFeature -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( ATTR_CURRENT_HUMIDITY, ATTR_CURRENT_TEMPERATURE, ATTR_FAN_MODE, @@ -41,6 +40,7 @@ from homeassistant.components.climate.const import ( SWING_OFF, SWING_ON, SWING_VERTICAL, + ClimateEntityFeature, HVACAction, HVACMode, ) diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index b7af7d516dd..445b73cccbe 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -40,7 +40,7 @@ from homeassistant.const import ( from homeassistant.core import Event, HomeAssistant, State, callback, split_entity_id import homeassistant.helpers.config_validation as cv from homeassistant.helpers.storage import STORAGE_DIR -import homeassistant.util.temperature as temp_util +from homeassistant.util.unit_conversion import TemperatureConverter from .const import ( AUDIO_CODEC_COPY, @@ -391,12 +391,12 @@ def cleanup_name_for_homekit(name: str | None) -> str: def temperature_to_homekit(temperature: float | int, unit: str) -> float: """Convert temperature to Celsius for HomeKit.""" - return round(temp_util.convert(temperature, unit, TEMP_CELSIUS), 1) + return round(TemperatureConverter.convert(temperature, unit, TEMP_CELSIUS), 1) def temperature_to_states(temperature: float | int, unit: str) -> float: """Convert temperature back from Celsius to Home Assistant unit.""" - return round(temp_util.convert(temperature, TEMP_CELSIUS, unit) * 2) / 2 + return round(TemperatureConverter.convert(temperature, TEMP_CELSIUS, unit) * 2) / 2 def density_to_air_quality(density: float) -> int: diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index dca626d2abd..dac4afc0b22 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +import contextlib import logging import aiohomekit @@ -41,7 +42,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await conn.async_setup() except (AccessoryNotFoundError, EncryptionError, AccessoryDisconnectedError) as ex: del hass.data[KNOWN_DEVICES][conn.unique_id] - await conn.pairing.close() + with contextlib.suppress(asyncio.TimeoutError): + await conn.pairing.close() raise ConfigEntryNotReady from ex return True diff --git a/homeassistant/components/homekit_controller/binary_sensor.py b/homeassistant/components/homekit_controller/binary_sensor.py index 11c81e7e251..c980e31b50c 100644 --- a/homeassistant/components/homekit_controller/binary_sensor.py +++ b/homeassistant/components/homekit_controller/binary_sensor.py @@ -10,9 +10,11 @@ from homeassistant.components.binary_sensor import ( ) 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 . import KNOWN_DEVICES +from .connection import HKDevice from .entity import HomeKitEntity @@ -106,6 +108,29 @@ class HomeKitLeakSensor(HomeKitEntity, BinarySensorEntity): return self.service.value(CharacteristicsTypes.LEAK_DETECTED) == 1 +class HomeKitBatteryLowSensor(HomeKitEntity, BinarySensorEntity): + """Representation of a Homekit battery low sensor.""" + + _attr_device_class = BinarySensorDeviceClass.BATTERY + _attr_entity_category = EntityCategory.DIAGNOSTIC + + def get_characteristic_types(self) -> list[str]: + """Define the homekit characteristics the entity is tracking.""" + return [CharacteristicsTypes.STATUS_LO_BATT] + + @property + def name(self) -> str: + """Return the name of the sensor.""" + if name := self.accessory.name: + return f"{name} Low Battery" + return "Low Battery" + + @property + def is_on(self) -> bool: + """Return true if low battery is detected from the binary sensor.""" + return self.service.value(CharacteristicsTypes.STATUS_LO_BATT) == 1 + + ENTITY_TYPES = { ServicesTypes.MOTION_SENSOR: HomeKitMotionSensor, ServicesTypes.CONTACT_SENSOR: HomeKitContactSensor, @@ -113,6 +138,17 @@ ENTITY_TYPES = { ServicesTypes.CARBON_MONOXIDE_SENSOR: HomeKitCarbonMonoxideSensor, ServicesTypes.OCCUPANCY_SENSOR: HomeKitOccupancySensor, ServicesTypes.LEAK_SENSOR: HomeKitLeakSensor, + ServicesTypes.BATTERY_SERVICE: HomeKitBatteryLowSensor, +} + +# Only create the entity if it has the required characteristic +REQUIRED_CHAR_BY_TYPE = { + ServicesTypes.BATTERY_SERVICE: CharacteristicsTypes.STATUS_LO_BATT, +} +# Reject the service as another platform can represent it better +# if it has a specific characteristic +REJECT_CHAR_BY_TYPE = { + ServicesTypes.BATTERY_SERVICE: CharacteristicsTypes.BATTERY_LEVEL, } @@ -123,12 +159,20 @@ async def async_setup_entry( ) -> None: """Set up Homekit lighting.""" hkid = config_entry.data["AccessoryPairingID"] - conn = hass.data[KNOWN_DEVICES][hkid] + conn: HKDevice = hass.data[KNOWN_DEVICES][hkid] @callback def async_add_service(service: Service) -> bool: if not (entity_class := ENTITY_TYPES.get(service.type)): return False + if ( + required_char := REQUIRED_CHAR_BY_TYPE.get(service.type) + ) and not service.has(required_char): + return False + if (reject_char := REJECT_CHAR_BY_TYPE.get(service.type)) and service.has( + reject_char + ): + return False info = {"aid": service.accessory.aid, "iid": service.iid} async_add_entities([entity_class(conn, info)], True) return True diff --git a/homeassistant/components/homekit_controller/climate.py b/homeassistant/components/homekit_controller/climate.py index 7254363e835..2c4d2e3871d 100644 --- a/homeassistant/components/homekit_controller/climate.py +++ b/homeassistant/components/homekit_controller/climate.py @@ -17,18 +17,16 @@ from aiohomekit.model.services import Service, ServicesTypes from aiohomekit.utils import clamp_enum_to_char from homeassistant.components.climate import ( - DEFAULT_MAX_TEMP, - DEFAULT_MIN_TEMP, - ClimateEntity, -) -from homeassistant.components.climate.const import ( ATTR_HVAC_MODE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, + DEFAULT_MAX_TEMP, + DEFAULT_MIN_TEMP, FAN_AUTO, FAN_ON, SWING_OFF, SWING_VERTICAL, + ClimateEntity, ClimateEntityFeature, HVACAction, HVACMode, diff --git a/homeassistant/components/homekit_controller/connection.py b/homeassistant/components/homekit_controller/connection.py index 8afbe6a70e4..05a0a589bf1 100644 --- a/homeassistant/components/homekit_controller/connection.py +++ b/homeassistant/components/homekit_controller/connection.py @@ -231,6 +231,9 @@ class HKDevice: self.async_update_available_state, timedelta(seconds=BLE_AVAILABILITY_CHECK_INTERVAL), ) + # BLE devices always get an RSSI sensor as well + if "sensor" not in self.platforms: + await self.async_load_platform("sensor") async def async_add_new_entities(self) -> None: """Add new entities to Home Assistant.""" @@ -455,7 +458,7 @@ class HKDevice: self.entities.append((accessory.aid, None, None)) break - def add_char_factory(self, add_entities_cb) -> None: + def add_char_factory(self, add_entities_cb: AddCharacteristicCb) -> None: """Add a callback to run when discovering new entities for accessories.""" self.char_factories.append(add_entities_cb) self._add_new_entities_for_char([add_entities_cb]) @@ -471,7 +474,7 @@ class HKDevice: self.entities.append((accessory.aid, service.iid, char.iid)) break - def add_listener(self, add_entities_cb) -> None: + def add_listener(self, add_entities_cb: AddServiceCb) -> None: """Add a callback to run when discovering new entities for services.""" self.listeners.append(add_entities_cb) self._add_new_entities([add_entities_cb]) @@ -513,22 +516,24 @@ class HKDevice: async def async_load_platforms(self) -> None: """Load any platforms needed by this HomeKit device.""" - tasks = [] + to_load: set[str] = set() for accessory in self.entity_map.accessories: for service in accessory.services: if service.type in HOMEKIT_ACCESSORY_DISPATCH: platform = HOMEKIT_ACCESSORY_DISPATCH[service.type] if platform not in self.platforms: - tasks.append(self.async_load_platform(platform)) + to_load.add(platform) for char in service.characteristics: if char.type in CHARACTERISTIC_PLATFORMS: platform = CHARACTERISTIC_PLATFORMS[char.type] if platform not in self.platforms: - tasks.append(self.async_load_platform(platform)) + to_load.add(platform) - if tasks: - await asyncio.gather(*tasks) + if to_load: + await asyncio.gather( + *[self.async_load_platform(platform) for platform in to_load] + ) @callback def async_update_available_state(self, *_: Any) -> None: diff --git a/homeassistant/components/homekit_controller/humidifier.py b/homeassistant/components/homekit_controller/humidifier.py index ebba525e0c9..adc1b1c7935 100644 --- a/homeassistant/components/homekit_controller/humidifier.py +++ b/homeassistant/components/homekit_controller/humidifier.py @@ -7,15 +7,13 @@ from aiohomekit.model.characteristics import CharacteristicsTypes from aiohomekit.model.services import Service, ServicesTypes from homeassistant.components.humidifier import ( - HumidifierDeviceClass, - HumidifierEntity, - HumidifierEntityFeature, -) -from homeassistant.components.humidifier.const import ( DEFAULT_MAX_HUMIDITY, DEFAULT_MIN_HUMIDITY, MODE_AUTO, MODE_NORMAL, + HumidifierDeviceClass, + HumidifierEntity, + HumidifierEntityFeature, ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 7ecd54e0a79..2f5f8911968 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==1.5.12"], + "requirements": ["aiohomekit==2.0.1"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }], "dependencies": ["bluetooth", "zeroconf"], diff --git a/homeassistant/components/homekit_controller/media_player.py b/homeassistant/components/homekit_controller/media_player.py index 092652ed17d..5c791f165e2 100644 --- a/homeassistant/components/homekit_controller/media_player.py +++ b/homeassistant/components/homekit_controller/media_player.py @@ -16,15 +16,9 @@ from homeassistant.components.media_player import ( MediaPlayerDeviceClass, MediaPlayerEntity, MediaPlayerEntityFeature, + MediaPlayerState, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - STATE_IDLE, - STATE_OK, - STATE_PAUSED, - STATE_PLAYING, - STATE_PROBLEM, -) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -35,9 +29,9 @@ _LOGGER = logging.getLogger(__name__) HK_TO_HA_STATE = { - CurrentMediaStateValues.PLAYING: STATE_PLAYING, - CurrentMediaStateValues.PAUSED: STATE_PAUSED, - CurrentMediaStateValues.STOPPED: STATE_IDLE, + CurrentMediaStateValues.PLAYING: MediaPlayerState.PLAYING, + CurrentMediaStateValues.PAUSED: MediaPlayerState.PAUSED, + CurrentMediaStateValues.STOPPED: MediaPlayerState.IDLE, } @@ -163,21 +157,21 @@ class HomeKitTelevision(HomeKitEntity, MediaPlayerEntity): return char.value @property - def state(self) -> str: + def state(self) -> MediaPlayerState: """State of the tv.""" active = self.service.value(CharacteristicsTypes.ACTIVE) if not active: - return STATE_PROBLEM + return MediaPlayerState.OFF homekit_state = self.service.value(CharacteristicsTypes.CURRENT_MEDIA_STATE) if homekit_state is not None: - return HK_TO_HA_STATE.get(homekit_state, STATE_OK) + return HK_TO_HA_STATE.get(homekit_state, MediaPlayerState.ON) - return STATE_OK + return MediaPlayerState.ON async def async_media_play(self) -> None: """Send play command.""" - if self.state == STATE_PLAYING: + if self.state == MediaPlayerState.PLAYING: _LOGGER.debug("Cannot play while already playing") return @@ -192,7 +186,7 @@ class HomeKitTelevision(HomeKitEntity, MediaPlayerEntity): async def async_media_pause(self) -> None: """Send pause command.""" - if self.state == STATE_PAUSED: + if self.state == MediaPlayerState.PAUSED: _LOGGER.debug("Cannot pause while already paused") return @@ -207,7 +201,7 @@ class HomeKitTelevision(HomeKitEntity, MediaPlayerEntity): async def async_media_stop(self) -> None: """Send stop command.""" - if self.state == STATE_IDLE: + if self.state == MediaPlayerState.IDLE: _LOGGER.debug("Cannot stop when already idle") return diff --git a/homeassistant/components/homekit_controller/number.py b/homeassistant/components/homekit_controller/number.py index 2987c82e829..6347ccb2a56 100644 --- a/homeassistant/components/homekit_controller/number.py +++ b/homeassistant/components/homekit_controller/number.py @@ -8,11 +8,12 @@ from __future__ import annotations from aiohomekit.model.characteristics import Characteristic, CharacteristicsTypes -from homeassistant.components.number import NumberEntity, NumberEntityDescription -from homeassistant.components.number.const import ( +from homeassistant.components.number import ( DEFAULT_MAX_VALUE, DEFAULT_MIN_VALUE, DEFAULT_STEP, + NumberEntity, + NumberEntityDescription, ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback @@ -59,7 +60,7 @@ async def async_setup_entry( ) -> None: """Set up Homekit numbers.""" hkid = config_entry.data["AccessoryPairingID"] - conn = hass.data[KNOWN_DEVICES][hkid] + conn: HKDevice = hass.data[KNOWN_DEVICES][hkid] @callback def async_add_characteristic(char: Characteristic) -> bool: diff --git a/homeassistant/components/homekit_controller/sensor.py b/homeassistant/components/homekit_controller/sensor.py index 04856a60347..564eb5ba9c6 100644 --- a/homeassistant/components/homekit_controller/sensor.py +++ b/homeassistant/components/homekit_controller/sensor.py @@ -3,11 +3,14 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass +import logging +from aiohomekit.model import Accessory, Transport from aiohomekit.model.characteristics import Characteristic, CharacteristicsTypes from aiohomekit.model.characteristics.const import ThreadNodeCapabilities, ThreadStatus from aiohomekit.model.services import Service, ServicesTypes +from homeassistant.components.bluetooth import async_ble_device_from_address from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, @@ -25,6 +28,7 @@ from homeassistant.const import ( PERCENTAGE, POWER_WATT, PRESSURE_HPA, + SIGNAL_STRENGTH_DECIBELS_MILLIWATT, TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant, callback @@ -37,6 +41,8 @@ from .connection import HKDevice from .entity import CharacteristicEntity, HomeKitEntity from .utils import folded_name +_LOGGER = logging.getLogger(__name__) + @dataclass class HomeKitSensorEntityDescription(SensorEntityDescription): @@ -410,6 +416,7 @@ class HomeKitBatterySensor(HomeKitSensor): _attr_device_class = SensorDeviceClass.BATTERY _attr_native_unit_of_measurement = PERCENTAGE + _attr_entity_category = EntityCategory.DIAGNOSTIC def get_characteristic_types(self) -> list[str]: """Define the homekit characteristics the entity is tracking.""" @@ -517,6 +524,50 @@ ENTITY_TYPES = { ServicesTypes.BATTERY_SERVICE: HomeKitBatterySensor, } +# Only create the entity if it has the required characteristic +REQUIRED_CHAR_BY_TYPE = { + ServicesTypes.BATTERY_SERVICE: CharacteristicsTypes.BATTERY_LEVEL, +} + + +class RSSISensor(HomeKitEntity, SensorEntity): + """HomeKit Controller RSSI sensor.""" + + _attr_device_class = SensorDeviceClass.SIGNAL_STRENGTH + _attr_entity_category = EntityCategory.DIAGNOSTIC + _attr_entity_registry_enabled_default = False + _attr_has_entity_name = True + _attr_native_unit_of_measurement = SIGNAL_STRENGTH_DECIBELS_MILLIWATT + _attr_should_poll = False + + def get_characteristic_types(self) -> list[str]: + """Define the homekit characteristics the entity cares about.""" + return [] + + @property + def available(self) -> bool: + """Return if the bluetooth device is available.""" + address = self._accessory.pairing_data["AccessoryAddress"] + return async_ble_device_from_address(self.hass, address) is not None + + @property + def name(self) -> str: + """Return the name of the sensor.""" + return "Signal strength" + + @property + def unique_id(self) -> str: + """Return the ID of this device.""" + serial = self.accessory_info.value(CharacteristicsTypes.SERIAL_NUMBER) + return f"homekit-{serial}-rssi" + + @property + def native_value(self) -> int | None: + """Return the current rssi value.""" + address = self._accessory.pairing_data["AccessoryAddress"] + ble_device = async_ble_device_from_address(self.hass, address) + return ble_device.rssi if ble_device else None + async def async_setup_entry( hass: HomeAssistant, @@ -525,14 +576,18 @@ async def async_setup_entry( ) -> None: """Set up Homekit sensors.""" hkid = config_entry.data["AccessoryPairingID"] - conn = hass.data[KNOWN_DEVICES][hkid] + conn: HKDevice = hass.data[KNOWN_DEVICES][hkid] @callback def async_add_service(service: Service) -> bool: if not (entity_class := ENTITY_TYPES.get(service.type)): return False + if ( + required_char := REQUIRED_CHAR_BY_TYPE.get(service.type) + ) and not service.has(required_char): + return False info = {"aid": service.accessory.aid, "iid": service.iid} - async_add_entities([entity_class(conn, info)], True) + async_add_entities([entity_class(conn, info)]) return True conn.add_listener(async_add_service) @@ -544,8 +599,22 @@ async def async_setup_entry( if description.probe and not description.probe(char): return False info = {"aid": char.service.accessory.aid, "iid": char.service.iid} - async_add_entities([SimpleSensor(conn, info, char, description)], True) + async_add_entities([SimpleSensor(conn, info, char, description)]) return True conn.add_char_factory(async_add_characteristic) + + @callback + def async_add_accessory(accessory: Accessory) -> bool: + if conn.pairing.transport != Transport.BLE: + return False + + accessory_info = accessory.services.first( + service_type=ServicesTypes.ACCESSORY_INFORMATION + ) + info = {"aid": accessory.aid, "iid": accessory_info.iid} + async_add_entities([RSSISensor(conn, info)]) + return True + + conn.add_accessory_factory(async_add_accessory) diff --git a/homeassistant/components/homekit_controller/translations/pt.json b/homeassistant/components/homekit_controller/translations/pt.json index febadec444c..857226a9cdb 100644 --- a/homeassistant/components/homekit_controller/translations/pt.json +++ b/homeassistant/components/homekit_controller/translations/pt.json @@ -25,7 +25,7 @@ "data": { "pairing_code": "C\u00f3digo de emparelhamento" }, - "description": "Introduza o c\u00f3digo de emparelhamento do seu HomeKit (no formato XXX-XX-XXX) para utilizar este acess\u00f3rio", + "description": "Introduza o c\u00f3digo de emparelhamento do seu HomeKit (no formato XXX-XX-XXX) para utilizar este acess\u00f3rio.", "title": "Emparelhar com o acess\u00f3rio HomeKit" }, "user": { diff --git a/homeassistant/components/homekit_controller/translations/sensor.id.json b/homeassistant/components/homekit_controller/translations/sensor.id.json index feb6a4f869b..2ba9de75815 100644 --- a/homeassistant/components/homekit_controller/translations/sensor.id.json +++ b/homeassistant/components/homekit_controller/translations/sensor.id.json @@ -1,12 +1,20 @@ { "state": { "homekit_controller__thread_node_capabilities": { + "border_router_capable": "Kemampuan Router Perbatasan", "full": "Perangkat Akhir Lengkap", "minimal": "Perangkat Akhir Minimal", - "none": "Tidak Ada" + "none": "Tidak Ada", + "router_eligible": "Perangkat Akhir yang Memenuhi Syarat Router", + "sleepy": "Perangkat Akhir yang Jenak" }, "homekit_controller__thread_status": { + "border_router": "Router Perbatasan", + "child": "Anakan", + "detached": "Terpisah", "disabled": "Dinonaktifkan", + "joining": "Bergabung", + "leader": "Kepala", "router": "Router" } } diff --git a/homeassistant/components/homekit_controller/translations/sensor.nl.json b/homeassistant/components/homekit_controller/translations/sensor.nl.json new file mode 100644 index 00000000000..be137558599 --- /dev/null +++ b/homeassistant/components/homekit_controller/translations/sensor.nl.json @@ -0,0 +1,12 @@ +{ + "state": { + "homekit_controller__thread_node_capabilities": { + "none": "Geen" + }, + "homekit_controller__thread_status": { + "detached": "Ontkoppeld", + "disabled": "Uitgeschakeld", + "router": "Router" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/sensor.pt.json b/homeassistant/components/homekit_controller/translations/sensor.pt.json new file mode 100644 index 00000000000..077ff565388 --- /dev/null +++ b/homeassistant/components/homekit_controller/translations/sensor.pt.json @@ -0,0 +1,10 @@ +{ + "state": { + "homekit_controller__thread_node_capabilities": { + "border_router_capable": "Capacidade de roteador de borda", + "full": "Dispositivo final completo", + "minimal": "Dispositivo final m\u00ednimo", + "none": "Nenhum" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homematic/climate.py b/homeassistant/components/homematic/climate.py index 2d106b90072..d597eca30cd 100644 --- a/homeassistant/components/homematic/climate.py +++ b/homeassistant/components/homematic/climate.py @@ -3,12 +3,12 @@ from __future__ import annotations from typing import Any -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( PRESET_BOOST, PRESET_COMFORT, PRESET_ECO, PRESET_NONE, + ClimateEntity, ClimateEntityFeature, HVACMode, ) diff --git a/homeassistant/components/homematic/sensor.py b/homeassistant/components/homematic/sensor.py index 456a10b7630..c7a78c7bbcf 100644 --- a/homeassistant/components/homematic/sensor.py +++ b/homeassistant/components/homematic/sensor.py @@ -170,6 +170,7 @@ SENSOR_DESCRIPTIONS: dict[str, SensorEntityDescription] = { "WIND_SPEED": SensorEntityDescription( key="WIND_SPEED", native_unit_of_measurement=SPEED_KILOMETERS_PER_HOUR, + device_class=SensorDeviceClass.SPEED, icon="mdi:weather-windy", ), "WIND_DIRECTION": SensorEntityDescription( diff --git a/homeassistant/components/homematicip_cloud/climate.py b/homeassistant/components/homematicip_cloud/climate.py index ae3ecf9dc9d..b3d2236f05a 100644 --- a/homeassistant/components/homematicip_cloud/climate.py +++ b/homeassistant/components/homematicip_cloud/climate.py @@ -9,12 +9,12 @@ from homematicip.base.enums import AbsenceType from homematicip.device import Switch from homematicip.functionalHomes import IndoorClimateHome -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( PRESET_AWAY, PRESET_BOOST, PRESET_ECO, PRESET_NONE, + ClimateEntity, ClimateEntityFeature, HVACAction, HVACMode, diff --git a/homeassistant/components/homematicip_cloud/sensor.py b/homeassistant/components/homematicip_cloud/sensor.py index 57a8b7bd714..bb9dd8021ed 100644 --- a/homeassistant/components/homematicip_cloud/sensor.py +++ b/homeassistant/components/homematicip_cloud/sensor.py @@ -344,6 +344,8 @@ class HomematicipEnergySensor(HomematicipGenericEntity, SensorEntity): class HomematicipWindspeedSensor(HomematicipGenericEntity, SensorEntity): """Representation of the HomematicIP wind speed sensor.""" + _attr_device_class = SensorDeviceClass.SPEED + def __init__(self, hap: HomematicipHAP, device) -> None: """Initialize the windspeed sensor.""" super().__init__(hap, device, post="Windspeed") diff --git a/homeassistant/components/homematicip_cloud/translations/fr.json b/homeassistant/components/homematicip_cloud/translations/fr.json index dd85878991f..a54b26acb71 100644 --- a/homeassistant/components/homematicip_cloud/translations/fr.json +++ b/homeassistant/components/homematicip_cloud/translations/fr.json @@ -21,7 +21,7 @@ "title": "Choisissez le point d'acc\u00e8s HomematicIP" }, "link": { - "description": "Appuyez sur le bouton bleu du point d'acc\u00e8s et sur le bouton Envoyer pour enregistrer HomematicIP avec Home Assistant. \n\n ![Emplacement du bouton sur le pont](/static/images/config_flows/config_homematicip_cloud.png)", + "description": "Appuyez sur le bouton bleu du point d'acc\u00e8s et sur le bouton \u00ab\u00a0Valider\u00a0\u00bb pour enregistrer HomematicIP aupr\u00e8s de Home Assistant. \n\n![Emplacement du bouton sur le pont](/static/images/config_flows/config_homematicip_cloud.png)", "title": "Lier le point d'acc\u00e8s" } } diff --git a/homeassistant/components/homewizard/sensor.py b/homeassistant/components/homewizard/sensor.py index a66a2664ae1..6fc1d38ec12 100644 --- a/homeassistant/components/homewizard/sensor.py +++ b/homeassistant/components/homewizard/sensor.py @@ -131,6 +131,7 @@ SENSORS: Final[tuple[SensorEntityDescription, ...]] = ( name="Total water usage", native_unit_of_measurement=VOLUME_CUBIC_METERS, icon="mdi:gauge", + device_class=SensorDeviceClass.VOLUME, state_class=SensorStateClass.TOTAL_INCREASING, ), ) diff --git a/homeassistant/components/homewizard/translations/cs.json b/homeassistant/components/homewizard/translations/cs.json index dc14c52ea9c..9e3a3155670 100644 --- a/homeassistant/components/homewizard/translations/cs.json +++ b/homeassistant/components/homewizard/translations/cs.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", "unknown_error": "Neo\u010dek\u00e1van\u00e1 chyba" }, "step": { diff --git a/homeassistant/components/homewizard/translations/ja.json b/homeassistant/components/homewizard/translations/ja.json index 51b11300936..f7e1e33ee5f 100644 --- a/homeassistant/components/homewizard/translations/ja.json +++ b/homeassistant/components/homewizard/translations/ja.json @@ -4,7 +4,7 @@ "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\u3044API\u30d0\u30fc\u30b8\u30e7\u30f3", + "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" }, "step": { diff --git a/homeassistant/components/honeywell/climate.py b/homeassistant/components/honeywell/climate.py index abcd1d6f340..64f87fd19ae 100644 --- a/homeassistant/components/honeywell/climate.py +++ b/homeassistant/components/honeywell/climate.py @@ -6,8 +6,7 @@ from typing import Any import somecomfort -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, DEFAULT_MAX_TEMP, @@ -17,6 +16,7 @@ from homeassistant.components.climate.const import ( FAN_ON, PRESET_AWAY, PRESET_NONE, + ClimateEntity, ClimateEntityFeature, HVACAction, HVACMode, diff --git a/homeassistant/components/horizon/media_player.py b/homeassistant/components/horizon/media_player.py index a19199eb5b3..c75d47d06eb 100644 --- a/homeassistant/components/horizon/media_player.py +++ b/homeassistant/components/horizon/media_player.py @@ -14,16 +14,10 @@ from homeassistant.components.media_player import ( PLATFORM_SCHEMA, MediaPlayerEntity, MediaPlayerEntityFeature, + MediaPlayerState, + MediaType, ) -from homeassistant.components.media_player.const import MEDIA_TYPE_CHANNEL -from homeassistant.const import ( - CONF_HOST, - CONF_NAME, - CONF_PORT, - STATE_OFF, - STATE_PAUSED, - STATE_PLAYING, -) +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT from homeassistant.core import HomeAssistant from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv @@ -110,63 +104,63 @@ class HorizonDevice(MediaPlayerEntity): """Update State using the media server running on the Horizon.""" try: if self._client.is_powered_on(): - self._state = STATE_PLAYING + self._state = MediaPlayerState.PLAYING else: - self._state = STATE_OFF + self._state = MediaPlayerState.OFF except OSError: - self._state = STATE_OFF + self._state = MediaPlayerState.OFF def turn_on(self) -> None: """Turn the device on.""" - if self._state == STATE_OFF: + if self._state == MediaPlayerState.OFF: self._send_key(self._keys.POWER) def turn_off(self) -> None: """Turn the device off.""" - if self._state != STATE_OFF: + if self._state != MediaPlayerState.OFF: self._send_key(self._keys.POWER) def media_previous_track(self) -> None: """Channel down.""" self._send_key(self._keys.CHAN_DOWN) - self._state = STATE_PLAYING + self._state = MediaPlayerState.PLAYING def media_next_track(self) -> None: """Channel up.""" self._send_key(self._keys.CHAN_UP) - self._state = STATE_PLAYING + self._state = MediaPlayerState.PLAYING def media_play(self) -> None: """Send play command.""" self._send_key(self._keys.PAUSE) - self._state = STATE_PLAYING + self._state = MediaPlayerState.PLAYING def media_pause(self) -> None: """Send pause command.""" self._send_key(self._keys.PAUSE) - self._state = STATE_PAUSED + self._state = MediaPlayerState.PAUSED def media_play_pause(self) -> None: """Send play/pause command.""" self._send_key(self._keys.PAUSE) - if self._state == STATE_PAUSED: - self._state = STATE_PLAYING + if self._state == MediaPlayerState.PAUSED: + self._state = MediaPlayerState.PLAYING else: - self._state = STATE_PAUSED + self._state = MediaPlayerState.PAUSED def play_media(self, media_type: str, media_id: str, **kwargs: Any) -> None: """Play media / switch to channel.""" - if MEDIA_TYPE_CHANNEL == media_type: + if MediaType.CHANNEL == media_type: try: self._select_channel(int(media_id)) - self._state = STATE_PLAYING + self._state = MediaPlayerState.PLAYING except ValueError: _LOGGER.error("Invalid channel: %s", media_id) else: _LOGGER.error( "Invalid media type %s. Supported type: %s", media_type, - MEDIA_TYPE_CHANNEL, + MediaType.CHANNEL, ) def _select_channel(self, channel): diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 7c8594bdd90..9e022ab0444 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -33,7 +33,12 @@ from homeassistant.util import ssl as ssl_util from .auth import async_setup_auth from .ban import setup_bans -from .const import KEY_AUTHENTICATED, KEY_HASS, KEY_HASS_USER # noqa: F401 +from .const import ( # noqa: F401 + KEY_AUTHENTICATED, + KEY_HASS, + KEY_HASS_REFRESH_TOKEN_ID, + KEY_HASS_USER, +) from .cors import setup_cors from .forwarded import async_setup_forwarded from .request_context import current_request, setup_request_context diff --git a/homeassistant/components/http/manifest.json b/homeassistant/components/http/manifest.json index 4391fd1acaf..26bf3dc31ce 100644 --- a/homeassistant/components/http/manifest.json +++ b/homeassistant/components/http/manifest.json @@ -5,5 +5,6 @@ "requirements": ["aiohttp_cors==0.7.0"], "codeowners": ["@home-assistant/core"], "quality_scale": "internal", - "iot_class": "local_push" + "iot_class": "local_push", + "integration_type": "system" } diff --git a/homeassistant/components/huawei_lte/device_tracker.py b/homeassistant/components/huawei_lte/device_tracker.py index 18d797aefc6..f97bda7481e 100644 --- a/homeassistant/components/huawei_lte/device_tracker.py +++ b/homeassistant/components/huawei_lte/device_tracker.py @@ -8,11 +8,11 @@ from typing import Any, cast from stringcase import snakecase -from homeassistant.components.device_tracker.config_entry import ScannerEntity -from homeassistant.components.device_tracker.const import ( +from homeassistant.components.device_tracker import ( DOMAIN as DEVICE_TRACKER_DOMAIN, SourceType, ) +from homeassistant.components.device_tracker.config_entry import ScannerEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry diff --git a/homeassistant/components/huawei_lte/notify.py b/homeassistant/components/huawei_lte/notify.py index 6e01158a800..d47249ccc51 100644 --- a/homeassistant/components/huawei_lte/notify.py +++ b/homeassistant/components/huawei_lte/notify.py @@ -11,6 +11,7 @@ from huawei_lte_api.exceptions import ResponseErrorException from homeassistant.components.notify import ATTR_TARGET, BaseNotificationService from homeassistant.const import CONF_RECIPIENT from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import Router from .const import ATTR_UNIQUE_ID, DOMAIN @@ -20,8 +21,8 @@ _LOGGER = logging.getLogger(__name__) async def async_get_service( hass: HomeAssistant, - config: dict[str, Any], - discovery_info: dict[str, Any] | None = None, + config: ConfigType, + discovery_info: DiscoveryInfoType | None = None, ) -> HuaweiLteSmsNotificationService | None: """Get the notification service.""" if discovery_info is None: diff --git a/homeassistant/components/hue/logbook.py b/homeassistant/components/hue/logbook.py index 412ca044b58..ce09c4c7ac9 100644 --- a/homeassistant/components/hue/logbook.py +++ b/homeassistant/components/hue/logbook.py @@ -3,10 +3,7 @@ from __future__ import annotations from collections.abc import Callable -from homeassistant.components.logbook.const import ( - LOGBOOK_ENTRY_MESSAGE, - LOGBOOK_ENTRY_NAME, -) +from homeassistant.components.logbook import LOGBOOK_ENTRY_MESSAGE, LOGBOOK_ENTRY_NAME from homeassistant.const import CONF_DEVICE_ID, CONF_EVENT, CONF_ID, CONF_TYPE from homeassistant.core import Event, HomeAssistant, callback from homeassistant.helpers import device_registry as dr diff --git a/homeassistant/components/hue/scene.py b/homeassistant/components/hue/scene.py index 1504e52e33d..5b0c17ebbf4 100644 --- a/homeassistant/components/hue/scene.py +++ b/homeassistant/components/hue/scene.py @@ -71,7 +71,7 @@ async def async_setup_entry( vol.Coerce(float), vol.Range(min=0, max=600) ), vol.Optional(ATTR_BRIGHTNESS): vol.All( - vol.Coerce(int), vol.Range(min=0, max=255) + vol.Coerce(int), vol.Range(min=1, max=255) ), }, "_async_activate", diff --git a/homeassistant/components/hue/services.yaml b/homeassistant/components/hue/services.yaml index 96a66be199f..790100373f4 100644 --- a/homeassistant/components/hue/services.yaml +++ b/homeassistant/components/hue/services.yaml @@ -60,5 +60,5 @@ activate_scene: advanced: true selector: number: - min: 0 + min: 1 max: 255 diff --git a/homeassistant/components/hue/translations/bg.json b/homeassistant/components/hue/translations/bg.json index 93617d8c0e5..242c902fde5 100644 --- a/homeassistant/components/hue/translations/bg.json +++ b/homeassistant/components/hue/translations/bg.json @@ -40,8 +40,13 @@ "3": "\u0422\u0440\u0435\u0442\u0438 \u0431\u0443\u0442\u043e\u043d", "4": "\u0427\u0435\u0442\u0432\u044a\u0440\u0442\u0438 \u0431\u0443\u0442\u043e\u043d", "button_3": "\u0422\u0440\u0435\u0442\u0438 \u0431\u0443\u0442\u043e\u043d", + "clock_wise": "\u0412\u044a\u0440\u0442\u0435\u043d\u0435 \u043f\u043e \u0447\u0430\u0441\u043e\u0432\u043d\u0438\u043a\u043e\u0432\u0430\u0442\u0430 \u0441\u0442\u0440\u0435\u043b\u043a\u0430", + "counter_clock_wise": "\u0412\u044a\u0440\u0442\u0435\u043d\u0435 \u043e\u0431\u0440\u0430\u0442\u043d\u043e \u043d\u0430 \u0447\u0430\u0441\u043e\u0432\u043d\u0438\u043a\u043e\u0432\u0430\u0442\u0430 \u0441\u0442\u0440\u0435\u043b\u043a\u0430", "double_buttons_1_3": "\u041f\u044a\u0440\u0432\u0438 \u0438 \u0442\u0440\u0435\u0442\u0438 \u0431\u0443\u0442\u043e\u043d\u0438", "double_buttons_2_4": "\u0412\u0442\u043e\u0440\u0438 \u0438 \u0447\u0435\u0442\u0432\u044a\u0440\u0442\u0438 \u0431\u0443\u0442\u043e\u043d\u0438" + }, + "trigger_type": { + "start": "\"{subtype}\" \u0435 \u043d\u0430\u0442\u0438\u0441\u043d\u0430\u0442 \u043f\u044a\u0440\u0432\u043e\u043d\u0430\u0447\u0430\u043b\u043d\u043e" } }, "options": { diff --git a/homeassistant/components/hue/translations/hu.json b/homeassistant/components/hue/translations/hu.json index af8f616190a..32f52c47a7b 100644 --- a/homeassistant/components/hue/translations/hu.json +++ b/homeassistant/components/hue/translations/hu.json @@ -1,19 +1,19 @@ { "config": { "abort": { - "all_configured": "M\u00e1r minden Philips Hue bridge konfigur\u00e1lt", + "all_configured": "M\u00e1r minden Philips Hue bridge konfigur\u00e1lva van", "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", "already_in_progress": "A be\u00e1ll\u00edt\u00e1si folyamat m\u00e1r el lett kezdve", "cannot_connect": "Sikertelen csatlakoz\u00e1s", "discover_timeout": "Nem tal\u00e1lhat\u00f3 a Hue bridge", "invalid_host": "\u00c9rv\u00e9nytelen c\u00edm", - "no_bridges": "Nem tal\u00e1lhat\u00f3 Philips Hue bridget", + "no_bridges": "Nem tal\u00e1lhat\u00f3 Philips Hue bridge", "not_hue_bridge": "Nem egy Hue Bridge", "unknown": "Ismeretlen hiba t\u00f6rt\u00e9nt" }, "error": { "linking": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt", - "register_failed": "Regisztr\u00e1ci\u00f3 nem siker\u00fclt, k\u00e9rem pr\u00f3b\u00e1lja \u00fajra" + "register_failed": "A regisztr\u00e1ci\u00f3 nem siker\u00fclt, k\u00e9rem pr\u00f3b\u00e1lja \u00fajra" }, "step": { "init": { diff --git a/homeassistant/components/hue/translations/ja.json b/homeassistant/components/hue/translations/ja.json index dcf38b0e48c..c017e094abd 100644 --- a/homeassistant/components/hue/translations/ja.json +++ b/homeassistant/components/hue/translations/ja.json @@ -55,14 +55,14 @@ }, "trigger_type": { "double_short_release": "\u4e21\u65b9\u306e \"{subtype}\" \u3092\u96e2\u3059", - "initial_press": "\u30dc\u30bf\u30f3 \"{subtype}\" \u6700\u521d\u306b\u62bc\u3055\u308c\u305f", - "long_release": "\u30dc\u30bf\u30f3 \"{subtype}\" \u96e2\u3057\u305f\u5f8c\u306b\u9577\u62bc\u3057", + "initial_press": "\u300c {subtype} \u300d\u304c\u6700\u521d\u306b\u62bc\u3055\u308c\u307e\u3057\u305f", + "long_release": "\u300c {subtype} \u300d\u3092\u9577\u62bc\u3057\u3057\u3066\u96e2\u3059", "remote_button_long_release": "\u9577\u62bc\u3057\u3059\u308b\u3068 \"{subtype}\" \u30dc\u30bf\u30f3\u304c\u30ea\u30ea\u30fc\u30b9\u3055\u308c\u308b", "remote_button_short_press": "\"{subtype}\" \u30dc\u30bf\u30f3\u304c\u62bc\u3055\u308c\u307e\u3057\u305f\u3002", "remote_button_short_release": "\"{subtype}\" \u30dc\u30bf\u30f3\u304c\u30ea\u30ea\u30fc\u30b9\u3055\u308c\u307e\u3057\u305f", "remote_double_button_long_press": "\u4e21\u65b9\u306e \"{subtype}\" \u306f\u9577\u62bc\u3057\u5f8c\u306b\u30ea\u30ea\u30fc\u30b9\u3055\u308c\u307e\u3057\u305f", "remote_double_button_short_press": "\u4e21\u65b9\u306e \"{subtype}\" \u3092\u96e2\u3059", - "repeat": "\u30dc\u30bf\u30f3 \"{subtype}\" \u3092\u62bc\u3057\u305f\u307e\u307e", + "repeat": "\u300c {subtype} \u300d\u304c\u62bc\u3055\u308c\u305f", "short_release": "\u30dc\u30bf\u30f3 \"{subtype}\" \u77ed\u62bc\u3057\u306e\u5f8c\u306b\u96e2\u3059", "start": "\"{subtype}\" \u304c\u6700\u521d\u306b\u62bc\u3055\u308c\u307e\u3057\u305f" } diff --git a/homeassistant/components/hue/translations/pl.json b/homeassistant/components/hue/translations/pl.json index abfb6fa2a05..ff8ab182dd4 100644 --- a/homeassistant/components/hue/translations/pl.json +++ b/homeassistant/components/hue/translations/pl.json @@ -44,6 +44,8 @@ "button_2": "drugi", "button_3": "trzeci", "button_4": "czwarty", + "clock_wise": "obr\u00f3t w prawo", + "counter_clock_wise": "obr\u00f3t w lewo", "dim_down": "zmniejszenie jasno\u015bci", "dim_up": "zwi\u0119kszenie jasno\u015bci", "double_buttons_1_3": "pierwszy i trzeci", @@ -61,7 +63,8 @@ "remote_double_button_long_press": "oba przyciski \"{subtype}\" zostan\u0105 zwolnione po d\u0142ugim naci\u015bni\u0119ciu", "remote_double_button_short_press": "oba przyciski \"{subtype}\" zostan\u0105 zwolnione", "repeat": "przycisk \"{subtype}\" zostanie przytrzymany", - "short_release": "przycisk \"{subtype}\" zostanie zwolniony po kr\u00f3tkim naci\u015bni\u0119ciu" + "short_release": "przycisk \"{subtype}\" zostanie zwolniony po kr\u00f3tkim naci\u015bni\u0119ciu", + "start": "\"{subtype}\" zostanie lekko naci\u015bni\u0119ty" } }, "options": { diff --git a/homeassistant/components/hue/translations/sv.json b/homeassistant/components/hue/translations/sv.json index 519c35370fa..1707e345983 100644 --- a/homeassistant/components/hue/translations/sv.json +++ b/homeassistant/components/hue/translations/sv.json @@ -44,6 +44,8 @@ "button_2": "Andra knappen", "button_3": "Tredje knappen", "button_4": "Fj\u00e4rde knappen", + "clock_wise": "Rotation medurs", + "counter_clock_wise": "Rotation moturs", "dim_down": "Dimma ned", "dim_up": "Dimma upp", "double_buttons_1_3": "F\u00f6rsta och tredje knapparna", @@ -61,7 +63,8 @@ "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" + "short_release": "Knappen \" {subtype} \" sl\u00e4pps efter kort tryckning", + "start": "\" {subtype} \" trycktes f\u00f6rst" } }, "options": { diff --git a/homeassistant/components/hue/translations/tr.json b/homeassistant/components/hue/translations/tr.json index df6d4e247a9..9e423bd3815 100644 --- a/homeassistant/components/hue/translations/tr.json +++ b/homeassistant/components/hue/translations/tr.json @@ -55,15 +55,15 @@ }, "trigger_type": { "double_short_release": "Her iki \"{subtype}\" de b\u0131rak\u0131ld\u0131", - "initial_press": "Ba\u015flang\u0131\u00e7ta \" {subtype} \" d\u00fc\u011fmesine bas\u0131ld\u0131", - "long_release": "\" {subtype} \" d\u00fc\u011fmesi uzun bas\u0131ld\u0131ktan sonra b\u0131rak\u0131ld\u0131", - "remote_button_long_release": "\" {subtype} \" d\u00fc\u011fmesi uzun bas\u0131ld\u0131ktan sonra b\u0131rak\u0131ld\u0131", - "remote_button_short_press": "\" {subtype} \" d\u00fc\u011fmesine bas\u0131ld\u0131", - "remote_button_short_release": "\" {subtype} \" d\u00fc\u011fmesi b\u0131rak\u0131ld\u0131", + "initial_press": "\" {subtype} \" ba\u015flang\u0131\u00e7ta bas\u0131ld\u0131", + "long_release": "\"{subtype}\" uzun s\u00fcren bask\u0131lar\u0131n ard\u0131ndan yay\u0131nland\u0131", + "remote_button_long_release": "\" {subtype} \" uzun bas\u0131\u015ftan sonra \u00e7\u0131kt\u0131", + "remote_button_short_press": "\" {subtype} \" bas\u0131ld\u0131", + "remote_button_short_release": "\" {subtype} \" yay\u0131nland\u0131", "remote_double_button_long_press": "Her iki \" {subtype} \" uzun bas\u0131\u015ftan sonra b\u0131rak\u0131ld\u0131", "remote_double_button_short_press": "Her iki \"{subtype}\" de b\u0131rak\u0131ld\u0131", - "repeat": "\" {subtype} \" d\u00fc\u011fmesi bas\u0131l\u0131 tutuldu", - "short_release": "K\u0131sa bas\u0131ld\u0131ktan sonra \"{subtype}\" d\u00fc\u011fmesi b\u0131rak\u0131ld\u0131", + "repeat": "\" {subtype} \" bas\u0131l\u0131 tutuldu", + "short_release": "\" {subtype} \" k\u0131sa bas\u0131\u015ftan sonra yay\u0131nland\u0131", "start": "\" {subtype} \" ba\u015flang\u0131\u00e7ta bas\u0131ld\u0131" } }, diff --git a/homeassistant/components/huisbaasje/__init__.py b/homeassistant/components/huisbaasje/__init__.py index fa810d823ca..a3d8863f566 100644 --- a/homeassistant/components/huisbaasje/__init__.py +++ b/homeassistant/components/huisbaasje/__init__.py @@ -3,7 +3,7 @@ from datetime import timedelta import logging import async_timeout -from huisbaasje import Huisbaasje, HuisbaasjeException +from energyflip import EnergyFlip, EnergyFlipException from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform @@ -31,7 +31,7 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Huisbaasje from a config entry.""" # Create the Huisbaasje client - huisbaasje = Huisbaasje( + energyflip = EnergyFlip( username=entry.data[CONF_USERNAME], password=entry.data[CONF_PASSWORD], source_types=SOURCE_TYPES, @@ -40,13 +40,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Attempt authentication. If this fails, an exception is thrown try: - await huisbaasje.authenticate() - except HuisbaasjeException as exception: + await energyflip.authenticate() + except EnergyFlipException as exception: _LOGGER.error("Authentication failed: %s", str(exception)) return False async def async_update_data(): - return await async_update_huisbaasje(huisbaasje) + return await async_update_huisbaasje(energyflip) # Create a coordinator for polling updates coordinator = DataUpdateCoordinator( @@ -80,17 +80,17 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return unload_ok -async def async_update_huisbaasje(huisbaasje): +async def async_update_huisbaasje(energyflip): """Update the data by performing a request to Huisbaasje.""" try: # Note: asyncio.TimeoutError and aiohttp.ClientError are already # handled by the data update coordinator. async with async_timeout.timeout(FETCH_TIMEOUT): - if not huisbaasje.is_authenticated(): + if not energyflip.is_authenticated(): _LOGGER.warning("Huisbaasje is unauthenticated. Reauthenticating") - await huisbaasje.authenticate() + await energyflip.authenticate() - current_measurements = await huisbaasje.current_measurements() + current_measurements = await energyflip.current_measurements() return { source_type: { @@ -112,7 +112,7 @@ async def async_update_huisbaasje(huisbaasje): } for source_type in SOURCE_TYPES } - except HuisbaasjeException as exception: + except EnergyFlipException as exception: raise UpdateFailed(f"Error communicating with API: {exception}") from exception diff --git a/homeassistant/components/huisbaasje/config_flow.py b/homeassistant/components/huisbaasje/config_flow.py index 4139b0d75c5..fc3a1c06a15 100644 --- a/homeassistant/components/huisbaasje/config_flow.py +++ b/homeassistant/components/huisbaasje/config_flow.py @@ -1,7 +1,7 @@ """Config flow for Huisbaasje integration.""" import logging -from huisbaasje import Huisbaasje, HuisbaasjeConnectionException, HuisbaasjeException +from energyflip import EnergyFlip, EnergyFlipConnectionException, EnergyFlipException import voluptuous as vol from homeassistant import config_entries @@ -31,10 +31,10 @@ class HuisbaasjeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): try: user_id = await self._validate_input(user_input) - except HuisbaasjeConnectionException as exception: + except EnergyFlipConnectionException as exception: _LOGGER.warning(exception) errors["base"] = "cannot_connect" - except HuisbaasjeException as exception: + except EnergyFlipException as exception: _LOGGER.warning(exception) errors["base"] = "invalid_auth" except AbortFlow: @@ -72,9 +72,12 @@ class HuisbaasjeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): username = user_input[CONF_USERNAME] password = user_input[CONF_PASSWORD] - huisbaasje = Huisbaasje(username, password) + energyflip = EnergyFlip(username, password) - # Attempt authentication. If this fails, an HuisbaasjeException will be thrown - await huisbaasje.authenticate() + # Attempt authentication. If this fails, an EnergyFlipException will be thrown + await energyflip.authenticate() - return huisbaasje.get_user_id() + # Request customer overview. This also sets the user id on the client + await energyflip.customer_overview() + + return energyflip.get_user_id() diff --git a/homeassistant/components/huisbaasje/const.py b/homeassistant/components/huisbaasje/const.py index 637ebd03a17..481f11b2a36 100644 --- a/homeassistant/components/huisbaasje/const.py +++ b/homeassistant/components/huisbaasje/const.py @@ -1,5 +1,5 @@ """Constants for the Huisbaasje integration.""" -from huisbaasje.const import ( +from energyflip.const import ( SOURCE_TYPE_ELECTRICITY, SOURCE_TYPE_ELECTRICITY_IN, SOURCE_TYPE_ELECTRICITY_IN_LOW, diff --git a/homeassistant/components/huisbaasje/manifest.json b/homeassistant/components/huisbaasje/manifest.json index bf3155ed9b8..2963a82512b 100644 --- a/homeassistant/components/huisbaasje/manifest.json +++ b/homeassistant/components/huisbaasje/manifest.json @@ -3,7 +3,7 @@ "name": "Huisbaasje", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/huisbaasje", - "requirements": ["huisbaasje-client==0.1.0"], + "requirements": ["energyflip-client==0.2.1"], "codeowners": ["@dennisschroer"], "iot_class": "cloud_polling", "loggers": ["huisbaasje"] diff --git a/homeassistant/components/humidifier/__init__.py b/homeassistant/components/humidifier/__init__.py index b5f02a6a5d3..1077e133b3a 100644 --- a/homeassistant/components/humidifier/__init__.py +++ b/homeassistant/components/humidifier/__init__.py @@ -38,6 +38,9 @@ from .const import ( # noqa: F401 DEVICE_CLASS_DEHUMIDIFIER, DEVICE_CLASS_HUMIDIFIER, DOMAIN, + MODE_AUTO, + MODE_AWAY, + MODE_NORMAL, SERVICE_SET_HUMIDITY, SERVICE_SET_MODE, SUPPORT_MODES, @@ -65,6 +68,8 @@ DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.Coerce(HumidifierDeviceClass)) # use the HumidifierDeviceClass enum instead. DEVICE_CLASSES = [cls.value for cls in HumidifierDeviceClass] +# mypy: disallow-any-generics + @bind_hass def is_on(hass, entity_id): @@ -77,7 +82,7 @@ def is_on(hass, entity_id): async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up humidifier devices.""" - component = hass.data[DOMAIN] = EntityComponent( + component = hass.data[DOMAIN] = EntityComponent[HumidifierEntity]( _LOGGER, DOMAIN, hass, SCAN_INTERVAL ) await component.async_setup(config) @@ -106,13 +111,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry.""" - component: EntityComponent = hass.data[DOMAIN] + component: EntityComponent[HumidifierEntity] = hass.data[DOMAIN] return await component.async_setup_entry(entry) async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - component: EntityComponent = hass.data[DOMAIN] + component: EntityComponent[HumidifierEntity] = hass.data[DOMAIN] return await component.async_unload_entry(entry) diff --git a/homeassistant/components/humidifier/manifest.json b/homeassistant/components/humidifier/manifest.json index b64065a2583..0cb84e08f0e 100644 --- a/homeassistant/components/humidifier/manifest.json +++ b/homeassistant/components/humidifier/manifest.json @@ -3,5 +3,6 @@ "name": "Humidifier", "documentation": "https://www.home-assistant.io/integrations/humidifier", "codeowners": ["@home-assistant/core", "@Shulyaka"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "entity" } diff --git a/homeassistant/components/hunterdouglas_powerview/manifest.json b/homeassistant/components/hunterdouglas_powerview/manifest.json index d3711313c1d..930e80733e0 100644 --- a/homeassistant/components/hunterdouglas_powerview/manifest.json +++ b/homeassistant/components/hunterdouglas_powerview/manifest.json @@ -2,7 +2,7 @@ "domain": "hunterdouglas_powerview", "name": "Hunter Douglas PowerView", "documentation": "https://www.home-assistant.io/integrations/hunterdouglas_powerview", - "requirements": ["aiopvapi==2.0.0"], + "requirements": ["aiopvapi==2.0.2"], "codeowners": ["@bdraco", "@kingy444", "@trullock"], "config_flow": true, "homekit": { diff --git a/homeassistant/components/hvv_departures/sensor.py b/homeassistant/components/hvv_departures/sensor.py index 93e1002edf4..0a516529386 100644 --- a/homeassistant/components/hvv_departures/sensor.py +++ b/homeassistant/components/hvv_departures/sensor.py @@ -8,7 +8,7 @@ from pygti.exceptions import InvalidAuth from homeassistant.components.sensor import SensorDeviceClass, SensorEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_ATTRIBUTION, ATTR_ID +from homeassistant.const import ATTR_ID from homeassistant.core import HomeAssistant from homeassistant.helpers import aiohttp_client from homeassistant.helpers.entity import DeviceInfo @@ -54,18 +54,25 @@ async def async_setup_entry( class HVVDepartureSensor(SensorEntity): """HVVDepartureSensor class.""" + _attr_attribution = ATTRIBUTION + _attr_device_class = SensorDeviceClass.TIMESTAMP + _attr_icon = ICON + def __init__(self, hass, config_entry, session, hub): """Initialize.""" self.config_entry = config_entry self.station_name = self.config_entry.data[CONF_STATION]["name"] - self.attr = {ATTR_ATTRIBUTION: ATTRIBUTION} - self._available = False - self._state = None - self._name = f"Departures at {self.station_name}" + self._attr_extra_state_attributes = {} + self._attr_available = False + self._attr_name = f"Departures at {self.station_name}" self._last_error = None self.gti = hub.gti + station_id = config_entry.data[CONF_STATION]["id"] + station_type = config_entry.data[CONF_STATION]["type"] + self._attr_unique_id = f"{config_entry.entry_id}-{station_id}-{station_type}" + @Throttle(MIN_TIME_BETWEEN_UPDATES) async def async_update(self, **kwargs: Any) -> None: """Update the sensor.""" @@ -95,20 +102,20 @@ class HVVDepartureSensor(SensorEntity): if self._last_error != InvalidAuth: _LOGGER.error("Authentication failed: %r", error) self._last_error = InvalidAuth - self._available = False + self._attr_available = False except ClientConnectorError as error: if self._last_error != ClientConnectorError: _LOGGER.warning("Network unavailable: %r", error) self._last_error = ClientConnectorError - self._available = False + self._attr_available = False except Exception as error: # pylint: disable=broad-except if self._last_error != error: _LOGGER.error("Error occurred while fetching data: %r", error) self._last_error = error - self._available = False + self._attr_available = False if not (data["returnCode"] == "OK" and data.get("departures")): - self._available = False + self._attr_available = False return if self._last_error == ClientConnectorError: @@ -119,14 +126,14 @@ class HVVDepartureSensor(SensorEntity): departure = data["departures"][0] line = departure["line"] delay = departure.get("delay", 0) - self._available = True - self._state = ( + self._attr_available = True + self._attr_native_value = ( departure_time + timedelta(minutes=departure["timeOffset"]) + timedelta(seconds=delay) ) - self.attr.update( + self._attr_extra_state_attributes.update( { ATTR_LINE: line["name"], ATTR_ORIGIN: line["origin"], @@ -154,15 +161,7 @@ class HVVDepartureSensor(SensorEntity): ATTR_DELAY: delay, } ) - self.attr[ATTR_NEXT] = departures - - @property - def unique_id(self): - """Return a unique ID to use for this sensor.""" - station_id = self.config_entry.data[CONF_STATION]["id"] - station_type = self.config_entry.data[CONF_STATION]["type"] - - return f"{self.config_entry.entry_id}-{station_id}-{station_type}" + self._attr_extra_state_attributes[ATTR_NEXT] = departures @property def device_info(self): @@ -177,35 +176,5 @@ class HVVDepartureSensor(SensorEntity): ) }, manufacturer=MANUFACTURER, - name=self._name, + name=self.name, ) - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def native_value(self): - """Return the state of the sensor.""" - return self._state - - @property - def icon(self): - """Return the icon of the sensor.""" - return ICON - - @property - def available(self): - """Return True if entity is available.""" - return self._available - - @property - def device_class(self): - """Return the class of this device, from component DEVICE_CLASSES.""" - return SensorDeviceClass.TIMESTAMP - - @property - def extra_state_attributes(self): - """Return the state attributes.""" - return self.attr diff --git a/homeassistant/components/hyperion/translations/es.json b/homeassistant/components/hyperion/translations/es.json index 3220866ab16..fa2bddc95fa 100644 --- a/homeassistant/components/hyperion/translations/es.json +++ b/homeassistant/components/hyperion/translations/es.json @@ -8,7 +8,7 @@ "auth_required_error": "No se pudo determinar si se requiere autorizaci\u00f3n", "cannot_connect": "No se pudo conectar", "no_id": "La instancia de Hyperion Ambilight no inform\u00f3 su id", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "error": { "cannot_connect": "No se pudo conectar", diff --git a/homeassistant/components/iaqualink/climate.py b/homeassistant/components/iaqualink/climate.py index 5a8cd0ce09f..725f1b9084e 100644 --- a/homeassistant/components/iaqualink/climate.py +++ b/homeassistant/components/iaqualink/climate.py @@ -12,9 +12,9 @@ from iaqualink.const import ( ) from iaqualink.device import AqualinkHeater, AqualinkPump, AqualinkSensor, AqualinkState -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( DOMAIN as CLIMATE_DOMAIN, + ClimateEntity, ClimateEntityFeature, HVACMode, ) diff --git a/homeassistant/components/ibeacon/__init__.py b/homeassistant/components/ibeacon/__init__.py new file mode 100644 index 00000000000..2c191211910 --- /dev/null +++ b/homeassistant/components/ibeacon/__init__.py @@ -0,0 +1,36 @@ +"""The iBeacon tracker integration.""" +from __future__ import annotations + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceEntry, async_get + +from .const import DOMAIN, PLATFORMS +from .coordinator import IBeaconCoordinator + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Bluetooth LE Tracker from a config entry.""" + coordinator = hass.data[DOMAIN] = IBeaconCoordinator(hass, entry, async_get(hass)) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + coordinator.async_start() + 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.pop(DOMAIN) + return unload_ok + + +async def async_remove_config_entry_device( + hass: HomeAssistant, config_entry: ConfigEntry, device_entry: DeviceEntry +) -> bool: + """Remove iBeacon config entry from a device.""" + coordinator: IBeaconCoordinator = hass.data[DOMAIN] + return not any( + identifier + for identifier in device_entry.identifiers + if identifier[0] == DOMAIN and coordinator.async_device_id_seen(identifier[1]) + ) diff --git a/homeassistant/components/ibeacon/config_flow.py b/homeassistant/components/ibeacon/config_flow.py new file mode 100644 index 00000000000..f4d36c2e617 --- /dev/null +++ b/homeassistant/components/ibeacon/config_flow.py @@ -0,0 +1,31 @@ +"""Config flow for iBeacon Tracker integration.""" +from __future__ import annotations + +from typing import Any + +from homeassistant import config_entries +from homeassistant.components import bluetooth +from homeassistant.data_entry_flow import FlowResult + +from .const import DOMAIN + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for iBeacon Tracker.""" + + 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 not bluetooth.async_scanner_count(self.hass, connectable=False): + return self.async_abort(reason="bluetooth_not_available") + + if user_input is not None: + return self.async_create_entry(title="iBeacon Tracker", data={}) + + return self.async_show_form(step_id="user") diff --git a/homeassistant/components/ibeacon/const.py b/homeassistant/components/ibeacon/const.py new file mode 100644 index 00000000000..7d1ab15da0a --- /dev/null +++ b/homeassistant/components/ibeacon/const.py @@ -0,0 +1,35 @@ +"""Constants for the iBeacon Tracker integration.""" + +from datetime import timedelta + +from homeassistant.const import Platform + +DOMAIN = "ibeacon" + +PLATFORMS = [Platform.DEVICE_TRACKER, Platform.SENSOR] + +SIGNAL_IBEACON_DEVICE_NEW = "ibeacon_tracker_new_device" +SIGNAL_IBEACON_DEVICE_UNAVAILABLE = "ibeacon_tracker_unavailable_device" +SIGNAL_IBEACON_DEVICE_SEEN = "ibeacon_seen_device" + +ATTR_UUID = "uuid" +ATTR_MAJOR = "major" +ATTR_MINOR = "minor" +ATTR_SOURCE = "source" + +UNAVAILABLE_TIMEOUT = 180 # Number of seconds we wait for a beacon to be seen before marking it unavailable + +# How often to update RSSI if it has changed +# and look for unavailable groups that use a random MAC address +UPDATE_INTERVAL = timedelta(seconds=60) + +# If a device broadcasts this many unique ids from the same address +# we will add it to the ignore list since its garbage data. +MAX_IDS = 10 + +# If a device broadcasts this many major minors for the same uuid +# we will add it to the ignore list since its garbage data. +MAX_IDS_PER_UUID = 50 + +CONF_IGNORE_ADDRESSES = "ignore_addresses" +CONF_IGNORE_UUIDS = "ignore_uuids" diff --git a/homeassistant/components/ibeacon/coordinator.py b/homeassistant/components/ibeacon/coordinator.py new file mode 100644 index 00000000000..9979cdf4fa8 --- /dev/null +++ b/homeassistant/components/ibeacon/coordinator.py @@ -0,0 +1,434 @@ +"""Tracking for iBeacon devices.""" +from __future__ import annotations + +from datetime import datetime +import time + +from ibeacon_ble import ( + APPLE_MFR_ID, + IBEACON_FIRST_BYTE, + IBEACON_SECOND_BYTE, + iBeaconAdvertisement, + is_ibeacon_service_info, + parse, +) + +from homeassistant.components import bluetooth +from homeassistant.components.bluetooth.match import BluetoothCallbackMatcher +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback +from homeassistant.helpers.device_registry import DeviceRegistry +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.event import async_track_time_interval + +from .const import ( + CONF_IGNORE_ADDRESSES, + CONF_IGNORE_UUIDS, + DOMAIN, + MAX_IDS, + MAX_IDS_PER_UUID, + SIGNAL_IBEACON_DEVICE_NEW, + SIGNAL_IBEACON_DEVICE_SEEN, + SIGNAL_IBEACON_DEVICE_UNAVAILABLE, + UNAVAILABLE_TIMEOUT, + UPDATE_INTERVAL, +) + +MONOTONIC_TIME = time.monotonic + + +def signal_unavailable(unique_id: str) -> str: + """Signal for the unique_id going unavailable.""" + return f"{SIGNAL_IBEACON_DEVICE_UNAVAILABLE}_{unique_id}" + + +def signal_seen(unique_id: str) -> str: + """Signal for the unique_id being seen.""" + return f"{SIGNAL_IBEACON_DEVICE_SEEN}_{unique_id}" + + +def make_short_address(address: str) -> str: + """Convert a Bluetooth address to a short address.""" + results = address.replace("-", ":").split(":") + return f"{results[-2].upper()}{results[-1].upper()}"[-4:] + + +@callback +def async_name( + service_info: bluetooth.BluetoothServiceInfoBleak, + ibeacon_advertisement: iBeaconAdvertisement, + unique_address: bool = False, +) -> str: + """Return a name for the device.""" + if service_info.address in ( + service_info.name, + service_info.name.replace("_", ":"), + ): + base_name = f"{ibeacon_advertisement.uuid}_{ibeacon_advertisement.major}_{ibeacon_advertisement.minor}" + else: + base_name = service_info.name + if unique_address: + short_address = make_short_address(service_info.address) + if not base_name.upper().endswith(short_address): + return f"{base_name} {short_address}" + return base_name + + +@callback +def _async_dispatch_update( + hass: HomeAssistant, + device_id: str, + service_info: bluetooth.BluetoothServiceInfoBleak, + ibeacon_advertisement: iBeaconAdvertisement, + new: bool, + unique_address: bool, +) -> None: + """Dispatch an update.""" + if new: + async_dispatcher_send( + hass, + SIGNAL_IBEACON_DEVICE_NEW, + device_id, + async_name(service_info, ibeacon_advertisement, unique_address), + ibeacon_advertisement, + ) + return + + async_dispatcher_send( + hass, + signal_seen(device_id), + ibeacon_advertisement, + ) + + +class IBeaconCoordinator: + """Set up the iBeacon Coordinator.""" + + def __init__( + self, hass: HomeAssistant, entry: ConfigEntry, registry: DeviceRegistry + ) -> None: + """Initialize the Coordinator.""" + self.hass = hass + self._entry = entry + self._dev_reg = registry + + # iBeacon devices that do not follow the spec + # and broadcast custom data in the major and minor fields + self._ignore_addresses: set[str] = set( + entry.data.get(CONF_IGNORE_ADDRESSES, []) + ) + # iBeacon devices that do not follow the spec + # and broadcast custom data in the major and minor fields + self._ignore_uuids: set[str] = set(entry.data.get(CONF_IGNORE_UUIDS, [])) + + # iBeacons with fixed MAC addresses + self._last_ibeacon_advertisement_by_unique_id: dict[ + str, iBeaconAdvertisement + ] = {} + self._group_ids_by_address: dict[str, set[str]] = {} + self._unique_ids_by_address: dict[str, set[str]] = {} + self._unique_ids_by_group_id: dict[str, set[str]] = {} + self._addresses_by_group_id: dict[str, set[str]] = {} + self._unavailable_trackers: dict[str, CALLBACK_TYPE] = {} + + # iBeacon with random MAC addresses + self._group_ids_random_macs: set[str] = set() + self._last_seen_by_group_id: dict[str, bluetooth.BluetoothServiceInfoBleak] = {} + self._unavailable_group_ids: set[str] = set() + + # iBeacons with random MAC addresses, fixed UUID, random major/minor + self._major_minor_by_uuid: dict[str, set[tuple[int, int]]] = {} + + @callback + def async_device_id_seen(self, device_id: str) -> bool: + """Return True if the device_id has been seen since boot.""" + return bool( + device_id in self._last_ibeacon_advertisement_by_unique_id + or device_id in self._last_seen_by_group_id + ) + + @callback + def _async_handle_unavailable( + self, service_info: bluetooth.BluetoothServiceInfoBleak + ) -> None: + """Handle unavailable devices.""" + address = service_info.address + self._async_cancel_unavailable_tracker(address) + for unique_id in self._unique_ids_by_address[address]: + async_dispatcher_send(self.hass, signal_unavailable(unique_id)) + + @callback + def _async_cancel_unavailable_tracker(self, address: str) -> None: + """Cancel unavailable tracking for an address.""" + self._unavailable_trackers.pop(address)() + + @callback + def _async_ignore_uuid(self, uuid: str) -> None: + """Ignore an UUID that does not follow the spec and any entities created by it.""" + self._ignore_uuids.add(uuid) + major_minor_by_uuid = self._major_minor_by_uuid.pop(uuid) + unique_ids_to_purge = set() + for major, minor in major_minor_by_uuid: + group_id = f"{uuid}_{major}_{minor}" + if unique_ids := self._unique_ids_by_group_id.pop(group_id, None): + unique_ids_to_purge.update(unique_ids) + for address in self._addresses_by_group_id.pop(group_id, []): + self._async_cancel_unavailable_tracker(address) + self._unique_ids_by_address.pop(address) + self._group_ids_by_address.pop(address) + self._async_purge_untrackable_entities(unique_ids_to_purge) + entry_data = self._entry.data + new_data = entry_data | {CONF_IGNORE_UUIDS: list(self._ignore_uuids)} + self.hass.config_entries.async_update_entry(self._entry, data=new_data) + + @callback + def _async_ignore_address(self, address: str) -> None: + """Ignore an address that does not follow the spec and any entities created by it.""" + self._ignore_addresses.add(address) + self._async_cancel_unavailable_tracker(address) + entry_data = self._entry.data + new_data = entry_data | {CONF_IGNORE_ADDRESSES: list(self._ignore_addresses)} + self.hass.config_entries.async_update_entry(self._entry, data=new_data) + self._async_purge_untrackable_entities(self._unique_ids_by_address[address]) + self._group_ids_by_address.pop(address) + self._unique_ids_by_address.pop(address) + + @callback + def _async_purge_untrackable_entities(self, unique_ids: set[str]) -> None: + """Remove entities that are no longer trackable.""" + for unique_id in unique_ids: + if device := self._dev_reg.async_get_device({(DOMAIN, unique_id)}): + self._dev_reg.async_remove_device(device.id) + self._last_ibeacon_advertisement_by_unique_id.pop(unique_id, None) + + @callback + def _async_convert_random_mac_tracking( + self, + group_id: str, + service_info: bluetooth.BluetoothServiceInfoBleak, + ibeacon_advertisement: iBeaconAdvertisement, + ) -> None: + """Switch to random mac tracking method when a group is using rotating mac addresses.""" + self._group_ids_random_macs.add(group_id) + self._async_purge_untrackable_entities(self._unique_ids_by_group_id[group_id]) + self._unique_ids_by_group_id.pop(group_id) + self._addresses_by_group_id.pop(group_id) + self._async_update_ibeacon_with_random_mac( + group_id, service_info, ibeacon_advertisement + ) + + def _async_track_ibeacon_with_unique_address( + self, address: str, group_id: str, unique_id: str + ) -> None: + """Track an iBeacon with a unique address.""" + self._unique_ids_by_address.setdefault(address, set()).add(unique_id) + self._group_ids_by_address.setdefault(address, set()).add(group_id) + + self._unique_ids_by_group_id.setdefault(group_id, set()).add(unique_id) + self._addresses_by_group_id.setdefault(group_id, set()).add(address) + + @callback + def _async_update_ibeacon( + self, + service_info: bluetooth.BluetoothServiceInfoBleak, + change: bluetooth.BluetoothChange, + ) -> None: + """Update from a bluetooth callback.""" + if service_info.address in self._ignore_addresses: + return + if not (ibeacon_advertisement := parse(service_info)): + return + + uuid_str = str(ibeacon_advertisement.uuid) + if uuid_str in self._ignore_uuids: + return + + major = ibeacon_advertisement.major + minor = ibeacon_advertisement.minor + major_minor_by_uuid = self._major_minor_by_uuid.setdefault(uuid_str, set()) + if len(major_minor_by_uuid) + 1 > MAX_IDS_PER_UUID: + self._async_ignore_uuid(uuid_str) + return + + major_minor_by_uuid.add((major, minor)) + group_id = f"{uuid_str}_{major}_{minor}" + + if group_id in self._group_ids_random_macs: + self._async_update_ibeacon_with_random_mac( + group_id, service_info, ibeacon_advertisement + ) + return + + self._async_update_ibeacon_with_unique_address( + group_id, service_info, ibeacon_advertisement + ) + + @callback + def _async_update_ibeacon_with_random_mac( + self, + group_id: str, + service_info: bluetooth.BluetoothServiceInfoBleak, + ibeacon_advertisement: iBeaconAdvertisement, + ) -> None: + """Update iBeacons with random mac addresses.""" + new = group_id not in self._last_seen_by_group_id + self._last_seen_by_group_id[group_id] = service_info + self._unavailable_group_ids.discard(group_id) + _async_dispatch_update( + self.hass, group_id, service_info, ibeacon_advertisement, new, False + ) + + @callback + def _async_update_ibeacon_with_unique_address( + self, + group_id: str, + service_info: bluetooth.BluetoothServiceInfoBleak, + ibeacon_advertisement: iBeaconAdvertisement, + ) -> None: + # Handle iBeacon with a fixed mac address + # and or detect if the iBeacon is using a rotating mac address + # and switch to random mac tracking method + address = service_info.address + unique_id = f"{group_id}_{address}" + new = unique_id not in self._last_ibeacon_advertisement_by_unique_id + # Reject creating new trackers if the name is not set + if new and ( + service_info.device.name is None + or service_info.device.name.replace("-", ":") == service_info.device.address + ): + return + self._last_ibeacon_advertisement_by_unique_id[unique_id] = ibeacon_advertisement + self._async_track_ibeacon_with_unique_address(address, group_id, unique_id) + if address not in self._unavailable_trackers: + self._unavailable_trackers[address] = bluetooth.async_track_unavailable( + self.hass, self._async_handle_unavailable, address + ) + # Some manufacturers violate the spec and flood us with random + # data (sometimes its temperature data). + # + # Once we see more than MAX_IDS from the same + # address we remove all the trackers for that address and add the + # address to the ignore list since we know its garbage data. + if len(self._group_ids_by_address[address]) >= MAX_IDS: + self._async_ignore_address(address) + return + + # Once we see more than MAX_IDS from the same + # group_id we remove all the trackers for that group_id + # as it means the addresses are being rotated. + if len(self._addresses_by_group_id[group_id]) >= MAX_IDS: + self._async_convert_random_mac_tracking( + group_id, service_info, ibeacon_advertisement + ) + return + + _async_dispatch_update( + self.hass, unique_id, service_info, ibeacon_advertisement, new, True + ) + + @callback + def _async_stop(self) -> None: + """Stop the Coordinator.""" + for cancel in self._unavailable_trackers.values(): + cancel() + self._unavailable_trackers.clear() + + @callback + def _async_check_unavailable_groups_with_random_macs(self) -> None: + """Check for random mac groups that have not been seen in a while and mark them as unavailable.""" + now = MONOTONIC_TIME() + gone_unavailable = [ + group_id + for group_id in self._group_ids_random_macs + if group_id not in self._unavailable_group_ids + and (service_info := self._last_seen_by_group_id.get(group_id)) + and now - service_info.time > UNAVAILABLE_TIMEOUT + ] + for group_id in gone_unavailable: + self._unavailable_group_ids.add(group_id) + async_dispatcher_send(self.hass, signal_unavailable(group_id)) + + @callback + def _async_update_rssi(self) -> None: + """Check to see if the rssi has changed and update any devices. + + We don't callback on RSSI changes so we need to check them + here and send them over the dispatcher periodically to + ensure the distance calculation is update. + """ + for ( + unique_id, + ibeacon_advertisement, + ) in self._last_ibeacon_advertisement_by_unique_id.items(): + address = unique_id.split("_")[-1] + if ( + service_info := bluetooth.async_last_service_info( + self.hass, address, connectable=False + ) + ) and service_info.rssi != ibeacon_advertisement.rssi: + ibeacon_advertisement.update_rssi(service_info.rssi) + async_dispatcher_send( + self.hass, + signal_seen(unique_id), + ibeacon_advertisement, + ) + + @callback + def _async_update(self, _now: datetime) -> None: + """Update the Coordinator.""" + self._async_check_unavailable_groups_with_random_macs() + self._async_update_rssi() + + @callback + def _async_restore_from_registry(self) -> None: + """Restore the state of the Coordinator from the device registry.""" + for device in self._dev_reg.devices.values(): + unique_id = None + for identifier in device.identifiers: + if identifier[0] == DOMAIN: + unique_id = identifier[1] + break + if not unique_id: + continue + # iBeacons with a fixed MAC address + if unique_id.count("_") == 3: + uuid, major, minor, address = unique_id.split("_") + group_id = f"{uuid}_{major}_{minor}" + self._async_track_ibeacon_with_unique_address( + address, group_id, unique_id + ) + # iBeacons with a random MAC address + elif unique_id.count("_") == 2: + uuid, major, minor = unique_id.split("_") + group_id = f"{uuid}_{major}_{minor}" + self._group_ids_random_macs.add(group_id) + + @callback + def async_start(self) -> None: + """Start the Coordinator.""" + self._async_restore_from_registry() + entry = self._entry + entry.async_on_unload( + bluetooth.async_register_callback( + self.hass, + self._async_update_ibeacon, + BluetoothCallbackMatcher( + connectable=False, + manufacturer_id=APPLE_MFR_ID, + manufacturer_data_start=[IBEACON_FIRST_BYTE, IBEACON_SECOND_BYTE], + ), # We will take data from any source + bluetooth.BluetoothScanningMode.PASSIVE, + ) + ) + entry.async_on_unload(self._async_stop) + # Replay any that are already there. + for service_info in bluetooth.async_discovered_service_info( + self.hass, connectable=False + ): + if is_ibeacon_service_info(service_info): + self._async_update_ibeacon( + service_info, bluetooth.BluetoothChange.ADVERTISEMENT + ) + entry.async_on_unload( + async_track_time_interval(self.hass, self._async_update, UPDATE_INTERVAL) + ) diff --git a/homeassistant/components/ibeacon/device_tracker.py b/homeassistant/components/ibeacon/device_tracker.py new file mode 100644 index 00000000000..4c9337e54ce --- /dev/null +++ b/homeassistant/components/ibeacon/device_tracker.py @@ -0,0 +1,94 @@ +"""Support for tracking iBeacon devices.""" +from __future__ import annotations + +from ibeacon_ble import iBeaconAdvertisement + +from homeassistant.components.device_tracker import SourceType +from homeassistant.components.device_tracker.config_entry import BaseTrackerEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import STATE_HOME, STATE_NOT_HOME +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN, SIGNAL_IBEACON_DEVICE_NEW +from .coordinator import IBeaconCoordinator +from .entity import IBeaconEntity + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up device tracker for iBeacon Tracker component.""" + coordinator: IBeaconCoordinator = hass.data[DOMAIN] + + @callback + def _async_device_new( + unique_id: str, + identifier: str, + ibeacon_advertisement: iBeaconAdvertisement, + ) -> None: + """Signal a new device.""" + async_add_entities( + [ + IBeaconTrackerEntity( + coordinator, + identifier, + unique_id, + ibeacon_advertisement, + ) + ] + ) + + entry.async_on_unload( + async_dispatcher_connect(hass, SIGNAL_IBEACON_DEVICE_NEW, _async_device_new) + ) + + +class IBeaconTrackerEntity(IBeaconEntity, BaseTrackerEntity): + """An iBeacon Tracker entity.""" + + def __init__( + self, + coordinator: IBeaconCoordinator, + identifier: str, + device_unique_id: str, + ibeacon_advertisement: iBeaconAdvertisement, + ) -> None: + """Initialize an iBeacon tracker entity.""" + super().__init__( + coordinator, identifier, device_unique_id, ibeacon_advertisement + ) + self._attr_unique_id = device_unique_id + self._active = True + + @property + def state(self) -> str: + """Return the state of the device.""" + return STATE_HOME if self._active else STATE_NOT_HOME + + @property + def source_type(self) -> SourceType: + """Return tracker source type.""" + return SourceType.BLUETOOTH_LE + + @property + def icon(self) -> str: + """Return device icon.""" + return "mdi:bluetooth-connect" if self._active else "mdi:bluetooth-off" + + @callback + def _async_seen( + self, + ibeacon_advertisement: iBeaconAdvertisement, + ) -> None: + """Update state.""" + self._active = True + self._ibeacon_advertisement = ibeacon_advertisement + self.async_write_ha_state() + + @callback + def _async_unavailable(self) -> None: + """Set unavailable.""" + self._active = False + self.async_write_ha_state() diff --git a/homeassistant/components/ibeacon/entity.py b/homeassistant/components/ibeacon/entity.py new file mode 100644 index 00000000000..4baa06dd617 --- /dev/null +++ b/homeassistant/components/ibeacon/entity.py @@ -0,0 +1,80 @@ +"""Support for iBeacon device sensors.""" +from __future__ import annotations + +from abc import abstractmethod + +from ibeacon_ble import iBeaconAdvertisement + +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import DeviceInfo, Entity + +from .const import ATTR_MAJOR, ATTR_MINOR, ATTR_SOURCE, ATTR_UUID, DOMAIN +from .coordinator import IBeaconCoordinator, signal_seen, signal_unavailable + + +class IBeaconEntity(Entity): + """An iBeacon entity.""" + + _attr_should_poll = False + _attr_has_entity_name = True + + def __init__( + self, + coordinator: IBeaconCoordinator, + identifier: str, + device_unique_id: str, + ibeacon_advertisement: iBeaconAdvertisement, + ) -> None: + """Initialize an iBeacon sensor entity.""" + self._device_unique_id = device_unique_id + self._coordinator = coordinator + self._ibeacon_advertisement = ibeacon_advertisement + self._attr_device_info = DeviceInfo( + name=identifier, + identifiers={(DOMAIN, device_unique_id)}, + ) + + @property + def extra_state_attributes( + self, + ) -> dict[str, str | int]: + """Return the device state attributes.""" + ibeacon_advertisement = self._ibeacon_advertisement + return { + ATTR_UUID: str(ibeacon_advertisement.uuid), + ATTR_MAJOR: ibeacon_advertisement.major, + ATTR_MINOR: ibeacon_advertisement.minor, + ATTR_SOURCE: ibeacon_advertisement.source, + } + + @abstractmethod + @callback + def _async_seen( + self, + ibeacon_advertisement: iBeaconAdvertisement, + ) -> None: + """Update state.""" + + @abstractmethod + @callback + def _async_unavailable(self) -> None: + """Set unavailable.""" + + async def async_added_to_hass(self) -> None: + """Register state update callbacks.""" + await super().async_added_to_hass() + self.async_on_remove( + async_dispatcher_connect( + self.hass, + signal_seen(self._device_unique_id), + self._async_seen, + ) + ) + self.async_on_remove( + async_dispatcher_connect( + self.hass, + signal_unavailable(self._device_unique_id), + self._async_unavailable, + ) + ) diff --git a/homeassistant/components/ibeacon/manifest.json b/homeassistant/components/ibeacon/manifest.json new file mode 100644 index 00000000000..a2b55a69403 --- /dev/null +++ b/homeassistant/components/ibeacon/manifest.json @@ -0,0 +1,12 @@ +{ + "domain": "ibeacon", + "name": "iBeacon Tracker", + "documentation": "https://www.home-assistant.io/integrations/ibeacon", + "dependencies": ["bluetooth"], + "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [2, 21] }], + "requirements": ["ibeacon_ble==0.7.3"], + "codeowners": ["@bdraco"], + "iot_class": "local_push", + "loggers": ["bleak"], + "config_flow": true +} diff --git a/homeassistant/components/ibeacon/sensor.py b/homeassistant/components/ibeacon/sensor.py new file mode 100644 index 00000000000..4684efdf142 --- /dev/null +++ b/homeassistant/components/ibeacon/sensor.py @@ -0,0 +1,137 @@ +"""Support for iBeacon device sensors.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass + +from ibeacon_ble import iBeaconAdvertisement + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import LENGTH_METERS, SIGNAL_STRENGTH_DECIBELS_MILLIWATT +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN, SIGNAL_IBEACON_DEVICE_NEW +from .coordinator import IBeaconCoordinator +from .entity import IBeaconEntity + + +@dataclass +class IBeaconRequiredKeysMixin: + """Mixin for required keys.""" + + value_fn: Callable[[iBeaconAdvertisement], int | None] + + +@dataclass +class IBeaconSensorEntityDescription(SensorEntityDescription, IBeaconRequiredKeysMixin): + """Describes iBeacon sensor entity.""" + + +SENSOR_DESCRIPTIONS = ( + IBeaconSensorEntityDescription( + key="rssi", + name="Signal Strength", + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + entity_registry_enabled_default=False, + value_fn=lambda ibeacon_advertisement: ibeacon_advertisement.rssi, + state_class=SensorStateClass.MEASUREMENT, + ), + IBeaconSensorEntityDescription( + key="power", + name="Power", + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + entity_registry_enabled_default=False, + value_fn=lambda ibeacon_advertisement: ibeacon_advertisement.power, + state_class=SensorStateClass.MEASUREMENT, + ), + IBeaconSensorEntityDescription( + key="estimated_distance", + name="Estimated Distance", + icon="mdi:signal-distance-variant", + native_unit_of_measurement=LENGTH_METERS, + value_fn=lambda ibeacon_advertisement: ibeacon_advertisement.distance, + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.DISTANCE, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up sensors for iBeacon Tracker component.""" + coordinator: IBeaconCoordinator = hass.data[DOMAIN] + + @callback + def _async_device_new( + unique_id: str, + identifier: str, + ibeacon_advertisement: iBeaconAdvertisement, + ) -> None: + """Signal a new device.""" + async_add_entities( + IBeaconSensorEntity( + coordinator, + description, + identifier, + unique_id, + ibeacon_advertisement, + ) + for description in SENSOR_DESCRIPTIONS + ) + + entry.async_on_unload( + async_dispatcher_connect(hass, SIGNAL_IBEACON_DEVICE_NEW, _async_device_new) + ) + + +class IBeaconSensorEntity(IBeaconEntity, SensorEntity): + """An iBeacon sensor entity.""" + + entity_description: IBeaconSensorEntityDescription + + def __init__( + self, + coordinator: IBeaconCoordinator, + description: IBeaconSensorEntityDescription, + identifier: str, + device_unique_id: str, + ibeacon_advertisement: iBeaconAdvertisement, + ) -> None: + """Initialize an iBeacon sensor entity.""" + super().__init__( + coordinator, identifier, device_unique_id, ibeacon_advertisement + ) + self._attr_unique_id = f"{device_unique_id}_{description.key}" + self.entity_description = description + + @callback + def _async_seen( + self, + ibeacon_advertisement: iBeaconAdvertisement, + ) -> None: + """Update state.""" + self._attr_available = True + self._ibeacon_advertisement = ibeacon_advertisement + self.async_write_ha_state() + + @callback + def _async_unavailable(self) -> None: + """Update state.""" + self._attr_available = False + self.async_write_ha_state() + + @property + def native_value(self) -> int | None: + """Return the state of the sensor.""" + return self.entity_description.value_fn(self._ibeacon_advertisement) diff --git a/homeassistant/components/ibeacon/strings.json b/homeassistant/components/ibeacon/strings.json new file mode 100644 index 00000000000..e2a1ab8393f --- /dev/null +++ b/homeassistant/components/ibeacon/strings.json @@ -0,0 +1,23 @@ +{ + "config": { + "step": { + "user": { + "description": "Do you want to setup iBeacon Tracker?" + } + }, + "abort": { + "bluetooth_not_available": "At least one Bluetooth adapter or remote must be configured to use iBeacon Tracker.", + "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" + } + }, + "options": { + "step": { + "init": { + "description": "iBeacons with an RSSI value lower than the Minimum RSSI will be ignored. If the integration is seeing neighboring iBeacons, increasing this value may help.", + "data": { + "min_rssi": "Minimum RSSI" + } + } + } + } +} diff --git a/homeassistant/components/ibeacon/translations/bg.json b/homeassistant/components/ibeacon/translations/bg.json new file mode 100644 index 00000000000..6f8f950076d --- /dev/null +++ b/homeassistant/components/ibeacon/translations/bg.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u0412\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e. \u0412\u044a\u0437\u043c\u043e\u0436\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f." + }, + "step": { + "user": { + "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 iBeacon Tracker?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ibeacon/translations/ca.json b/homeassistant/components/ibeacon/translations/ca.json new file mode 100644 index 00000000000..aaca36cd8dd --- /dev/null +++ b/homeassistant/components/ibeacon/translations/ca.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "bluetooth_not_available": "Almenys un adaptador Bluetooth o controlador remot ha d'estar configurat per utilitzar iBeacon Tracker.", + "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3." + }, + "step": { + "user": { + "description": "Vols configurar iBeacon Tracker?" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "min_rssi": "RSSI m\u00ednim" + }, + "description": "S'ignoraran els iBeacons amb un valor d'RSSI inferior al m\u00ednim. Si la integraci\u00f3 veu iBeacons propers, augmentar aquest valor pot ajudar." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ibeacon/translations/de.json b/homeassistant/components/ibeacon/translations/de.json new file mode 100644 index 00000000000..c91a821d4cb --- /dev/null +++ b/homeassistant/components/ibeacon/translations/de.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "bluetooth_not_available": "Mindestens ein Bluetooth-Adapter oder eine Fernbedienung muss f\u00fcr die Verwendung von iBeacon Tracker konfiguriert sein.", + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." + }, + "step": { + "user": { + "description": "M\u00f6chtest du den iBeacon Tracker einrichten?" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "min_rssi": "Mindest-RSSI" + }, + "description": "iBeacons mit einem RSSI-Wert, der unter dem Mindest-RSSI liegt, werden ignoriert. Wenn die Integration benachbarte iBeacons sieht, kann eine Erh\u00f6hung dieses Wertes helfen." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ibeacon/translations/el.json b/homeassistant/components/ibeacon/translations/el.json new file mode 100644 index 00000000000..ca9522da0b7 --- /dev/null +++ b/homeassistant/components/ibeacon/translations/el.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "bluetooth_not_available": "\u03a4\u03bf\u03c5\u03bb\u03ac\u03c7\u03b9\u03c3\u03c4\u03bf\u03bd \u03ad\u03bd\u03b1\u03c2 \u03c0\u03c1\u03bf\u03c3\u03b1\u03c1\u03bc\u03bf\u03b3\u03ad\u03b1\u03c2 Bluetooth \u03ae \u03ad\u03bd\u03b1 \u03c4\u03b7\u03bb\u03b5\u03c7\u03b5\u03b9\u03c1\u03b9\u03c3\u03c4\u03ae\u03c1\u03b9\u03bf \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03ad\u03c7\u03b5\u03b9 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03c7\u03c1\u03ae\u03c3\u03b7 \u03c4\u03bf\u03c5 iBeacon Tracker.", + "single_instance_allowed": "\u0388\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af. \u039c\u03cc\u03bd\u03bf \u03bc\u03af\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae." + }, + "step": { + "user": { + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf iBeacon Tracker;" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "min_rssi": "\u0395\u03bb\u03ac\u03c7\u03b9\u03c3\u03c4\u03bf RSSI" + }, + "description": "\u03a4\u03b1 iBeacons \u03bc\u03b5 \u03c4\u03b9\u03bc\u03ae RSSI \u03c7\u03b1\u03bc\u03b7\u03bb\u03cc\u03c4\u03b5\u03c1\u03b7 \u03b1\u03c0\u03cc \u03c4\u03b7\u03bd \u03b5\u03bb\u03ac\u03c7\u03b9\u03c3\u03c4\u03b7 \u03c4\u03b9\u03bc\u03ae RSSI \u03b8\u03b1 \u03b1\u03b3\u03bd\u03bf\u03b7\u03b8\u03bf\u03cd\u03bd. \u0395\u03ac\u03bd \u03b7 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03b2\u03bb\u03ad\u03c0\u03b5\u03b9 \u03b3\u03b5\u03b9\u03c4\u03bf\u03bd\u03b9\u03ba\u03ac iBeacons, \u03b7 \u03b1\u03cd\u03be\u03b7\u03c3\u03b7 \u03b1\u03c5\u03c4\u03ae\u03c2 \u03c4\u03b7\u03c2 \u03c4\u03b9\u03bc\u03ae\u03c2 \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03b2\u03bf\u03b7\u03b8\u03ae\u03c3\u03b5\u03b9." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ibeacon/translations/en.json b/homeassistant/components/ibeacon/translations/en.json new file mode 100644 index 00000000000..1125e778b19 --- /dev/null +++ b/homeassistant/components/ibeacon/translations/en.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "bluetooth_not_available": "At least one Bluetooth adapter or remote must be configured to use iBeacon Tracker.", + "single_instance_allowed": "Already configured. Only a single configuration possible." + }, + "step": { + "user": { + "description": "Do you want to setup iBeacon Tracker?" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "min_rssi": "Minimum RSSI" + }, + "description": "iBeacons with an RSSI value lower than the Minimum RSSI will be ignored. If the integration is seeing neighboring iBeacons, increasing this value may help." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ibeacon/translations/es.json b/homeassistant/components/ibeacon/translations/es.json new file mode 100644 index 00000000000..f13c33161ed --- /dev/null +++ b/homeassistant/components/ibeacon/translations/es.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "bluetooth_not_available": "Se debe configurar al menos un adaptador Bluetooth o un control remoto para usar iBeacon Tracker.", + "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." + }, + "step": { + "user": { + "description": "\u00bfQuieres configurar iBeacon Tracker?" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "min_rssi": "RSSI m\u00ednimo" + }, + "description": "Se ignorar\u00e1n los iBeacons con un valor de RSSI inferior al RSSI m\u00ednimo. Si la integraci\u00f3n est\u00e1 viendo iBeacons vecinos, aumentar este valor puede ayudar." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ibeacon/translations/fr.json b/homeassistant/components/ibeacon/translations/fr.json new file mode 100644 index 00000000000..c86904b872f --- /dev/null +++ b/homeassistant/components/ibeacon/translations/fr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." + }, + "step": { + "user": { + "description": "Voulez-vous configurer iBeacon Tracker\u00a0?" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "min_rssi": "RSSI minimal" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ibeacon/translations/he.json b/homeassistant/components/ibeacon/translations/he.json new file mode 100644 index 00000000000..d0c3523da94 --- /dev/null +++ b/homeassistant/components/ibeacon/translations/he.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ibeacon/translations/hu.json b/homeassistant/components/ibeacon/translations/hu.json new file mode 100644 index 00000000000..598076ba70f --- /dev/null +++ b/homeassistant/components/ibeacon/translations/hu.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "bluetooth_not_available": "Legal\u00e1bb egy Bluetooth-adaptert vagy \u00e1tad\u00f3t be kell \u00e1ll\u00edtani az iBeacon Tracker haszn\u00e1lat\u00e1hoz.", + "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." + }, + "step": { + "user": { + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani az iBeacon Tracker-t?" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "min_rssi": "Minimum RSSI" + }, + "description": "A minimum RSSI \u00e9rt\u00e9kn\u00e9l alacsonyabb RSSI \u00e9rt\u00e9kkel rendelkez\u0151 iBeacon-\u00f6ket a rendszer figyelmen k\u00edv\u00fcl hagyja. Ha az integr\u00e1ci\u00f3 szomsz\u00e9d iBeacon-okat is l\u00e1t, akkor ennek az \u00e9rt\u00e9knek a n\u00f6vel\u00e9se seg\u00edthet." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ibeacon/translations/id.json b/homeassistant/components/ibeacon/translations/id.json new file mode 100644 index 00000000000..730581f1033 --- /dev/null +++ b/homeassistant/components/ibeacon/translations/id.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "bluetooth_not_available": "Setidaknya satu adaptor Bluetooth atau remote harus dikonfigurasi untuk menggunakan iBeacon Tracker.", + "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." + }, + "step": { + "user": { + "description": "Ingin menyiapkan iBeacon Tracker?" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "min_rssi": "RSSI minimum" + }, + "description": "iBeacon dengan nilai RSSI lebih rendah dari RSSI Minimum akan diabaikan. Jika integrasi melihat iBeacon tetangga, meningkatkan nilai ini mungkin akan membantu." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ibeacon/translations/it.json b/homeassistant/components/ibeacon/translations/it.json new file mode 100644 index 00000000000..41b6f30a401 --- /dev/null +++ b/homeassistant/components/ibeacon/translations/it.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "bluetooth_not_available": "Per utilizzare iBeacon Tracker \u00e8 necessario configurare almeno un adattatore o un telecomando Bluetooth.", + "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione." + }, + "step": { + "user": { + "description": "Vuoi configurare iBeacon Tracker?" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "min_rssi": "RSSI minimo" + }, + "description": "Gli iBeacon con un valore RSSI inferiore all'RSSI minimo verranno ignorati. Se l'integrazione vede iBeacon vicini, aumentare questo valore pu\u00f2 essere d'aiuto." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ibeacon/translations/nl.json b/homeassistant/components/ibeacon/translations/nl.json new file mode 100644 index 00000000000..703ac8614c4 --- /dev/null +++ b/homeassistant/components/ibeacon/translations/nl.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ibeacon/translations/no.json b/homeassistant/components/ibeacon/translations/no.json new file mode 100644 index 00000000000..f198120c2c4 --- /dev/null +++ b/homeassistant/components/ibeacon/translations/no.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "bluetooth_not_available": "Minst \u00e9n Bluetooth-adapter eller fjernkontroll m\u00e5 konfigureres for \u00e5 bruke iBeacon Tracker.", + "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig." + }, + "step": { + "user": { + "description": "Vil du sette opp iBeacon Tracker?" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "min_rssi": "Minimum RSSI" + }, + "description": "iBeacons med en RSSI-verdi lavere enn Minimum RSSI vil bli ignorert. Hvis integrasjonen ser n\u00e6rliggende iBeacons, kan det hjelpe \u00e5 \u00f8ke denne verdien." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ibeacon/translations/pl.json b/homeassistant/components/ibeacon/translations/pl.json new file mode 100644 index 00000000000..4a98f2316ea --- /dev/null +++ b/homeassistant/components/ibeacon/translations/pl.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "bluetooth_not_available": "Co najmniej jeden adapter lub pilot Bluetooth musi by\u0107 skonfigurowany do korzystania z iBeacon Tracker.", + "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja." + }, + "step": { + "user": { + "description": "Chcesz skonfigurowa\u0107 iBeacon Tracker?" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "min_rssi": "Minimalny RSSI" + }, + "description": "Sygna\u0142y iBeacon o warto\u015bci RSSI ni\u017cszej ni\u017c Minimalny RSSI b\u0119d\u0105 ignorowane. Je\u015bli integracja widzi s\u0105siednie iBeacons, zwi\u0119kszenie tej warto\u015bci mo\u017ce pom\u00f3c." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ibeacon/translations/pt-BR.json b/homeassistant/components/ibeacon/translations/pt-BR.json new file mode 100644 index 00000000000..0dfe8a4d8cd --- /dev/null +++ b/homeassistant/components/ibeacon/translations/pt-BR.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "bluetooth_not_available": "Pelo menos um adaptador ou controle remoto Bluetooth deve ser configurado para usar o iBeacon Tracker.", + "single_instance_allowed": "J\u00e1 est\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "step": { + "user": { + "description": "Deseja configurar o iBeacon Tracker?" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "min_rssi": "RSSI m\u00ednimo" + }, + "description": "Os iBeacons com um valor RSSI inferior ao RSSI m\u00ednimo ser\u00e3o ignorados. Se a integra\u00e7\u00e3o estiver vendo iBeacons vizinhos, aumentar esse valor pode ajudar." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ibeacon/translations/ru.json b/homeassistant/components/ibeacon/translations/ru.json new file mode 100644 index 00000000000..a61ea54ceb6 --- /dev/null +++ b/homeassistant/components/ibeacon/translations/ru.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "bluetooth_not_available": "\u0414\u043b\u044f \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u044f iBeacon Tracker \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d \u0445\u043e\u0442\u044f \u0431\u044b \u043e\u0434\u0438\u043d \u0430\u0434\u0430\u043f\u0442\u0435\u0440 Bluetooth.", + "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e." + }, + "step": { + "user": { + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c iBeacon Tracker?" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "min_rssi": "\u041c\u0438\u043d\u0438\u043c\u0430\u043b\u044c\u043d\u044b\u0439 RSSI" + }, + "description": "iBeacons \u0441\u043e \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435\u043c RSSI \u043d\u0438\u0436\u0435 \u043c\u0438\u043d\u0438\u043c\u0430\u043b\u044c\u043d\u043e\u0433\u043e RSSI \u0431\u0443\u0434\u0443\u0442 \u0438\u0433\u043d\u043e\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c\u0441\u044f. \u0415\u0441\u043b\u0438 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0432\u0438\u0434\u0438\u0442 \u0441\u043e\u0441\u0435\u0434\u043d\u0438\u0435 iBeacons, \u0443\u0432\u0435\u043b\u0438\u0447\u0435\u043d\u0438\u0435 \u044d\u0442\u043e\u0433\u043e \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f \u043c\u043e\u0436\u0435\u0442 \u043f\u043e\u043c\u043e\u0447\u044c." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ibeacon/translations/zh-Hant.json b/homeassistant/components/ibeacon/translations/zh-Hant.json new file mode 100644 index 00000000000..66bb34df27e --- /dev/null +++ b/homeassistant/components/ibeacon/translations/zh-Hant.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "bluetooth_not_available": "\u5fc5\u9808\u81f3\u5c11\u8a2d\u5b9a\u4e00\u7d44\u85cd\u82bd\u50b3\u8f38\u5668\u6216\u9060\u7aef\u88dd\u7f6e\u65b9\u80fd\u4f7f\u7528 iBeacon Tracker\u3002", + "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" + }, + "step": { + "user": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a iBeacon Tracker\uff1f" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "min_rssi": "\u6700\u5c0f RSSI \u503c" + }, + "description": "\u4f4e\u65bc\u6700\u5c0f RSSI \u503c\u7684 iBeacons \u5c07\u6703\u906d\u5230\u5ffd\u7565\u3002\u5047\u5982\u6574\u5408\u5075\u6e2c\u5230\u9644\u8fd1\u7684 iBeacon\u3001\u589e\u52a0\u6b64\u6578\u503c\u53ef\u80fd\u6709\u6240\u5e6b\u52a9\u3002" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/icloud/device_tracker.py b/homeassistant/components/icloud/device_tracker.py index a273b7909e2..44297a21112 100644 --- a/homeassistant/components/icloud/device_tracker.py +++ b/homeassistant/components/icloud/device_tracker.py @@ -3,14 +3,13 @@ from __future__ import annotations from typing import Any -from homeassistant.components.device_tracker import AsyncSeeCallback, SourceType +from homeassistant.components.device_tracker import SourceType from homeassistant.components.device_tracker.config_entry import TrackerEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .account import IcloudAccount, IcloudDevice from .const import ( @@ -21,15 +20,6 @@ from .const import ( ) -async def async_setup_scanner( - hass: HomeAssistant, - config: ConfigType, - async_see: AsyncSeeCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> bool: - """Old way of setting up the iCloud tracker.""" - - async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: diff --git a/homeassistant/components/icloud/translations/bg.json b/homeassistant/components/icloud/translations/bg.json index bd81093beb0..3c54ef831d1 100644 --- a/homeassistant/components/icloud/translations/bg.json +++ b/homeassistant/components/icloud/translations/bg.json @@ -14,6 +14,13 @@ "password": "\u041f\u0430\u0440\u043e\u043b\u0430" } }, + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430" + }, + "description": "\u041f\u043e-\u0440\u0430\u043d\u043e \u0432\u044a\u0432\u0435\u0434\u0435\u043d\u0430\u0442\u0430 \u043f\u0430\u0440\u043e\u043b\u0430 \u0437\u0430 {username} \u0432\u0435\u0447\u0435 \u043d\u0435 \u0440\u0430\u0431\u043e\u0442\u0438. \u0410\u043a\u0442\u0443\u0430\u043b\u0438\u0437\u0438\u0440\u0430\u0439\u0442\u0435 \u043f\u0430\u0440\u043e\u043b\u0430\u0442\u0430 \u0441\u0438, \u0437\u0430 \u0434\u0430 \u043f\u0440\u043e\u0434\u044a\u043b\u0436\u0438\u0442\u0435 \u0434\u0430 \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u0442\u0435 \u0442\u0430\u0437\u0438 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f.", + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u043d\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430" + }, "user": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u0430", diff --git a/homeassistant/components/icloud/translations/ca.json b/homeassistant/components/icloud/translations/ca.json index 0ffdf5bc0c1..187617b23e7 100644 --- a/homeassistant/components/icloud/translations/ca.json +++ b/homeassistant/components/icloud/translations/ca.json @@ -18,6 +18,13 @@ "description": "La contrasenya introdu\u00efda anteriorment per a {username} ja no funciona. Actualitza la contrasenya per continuar utilitzant aquesta integraci\u00f3.", "title": "Reautenticaci\u00f3 de la integraci\u00f3" }, + "reauth_confirm": { + "data": { + "password": "Contrasenya" + }, + "description": "La contrasenya introdu\u00efda anteriorment per a {username} ja no funciona. Actualitza la contrasenya per continuar utilitzant aquesta integraci\u00f3.", + "title": "Reautenticaci\u00f3 de la integraci\u00f3" + }, "trusted_device": { "data": { "trusted_device": "Dispositiu de confian\u00e7a" diff --git a/homeassistant/components/icloud/translations/cs.json b/homeassistant/components/icloud/translations/cs.json index f06cae4019d..72dc892d15f 100644 --- a/homeassistant/components/icloud/translations/cs.json +++ b/homeassistant/components/icloud/translations/cs.json @@ -18,6 +18,12 @@ "description": "Va\u0161e zadan\u00e9 heslo pro {username} ji\u017e nefunguje. Chcete-li tuto d\u00e1le integraci pou\u017e\u00edvat, aktualizujte sv\u00e9 heslo.", "title": "Znovu ov\u011b\u0159it integraci" }, + "reauth_confirm": { + "data": { + "password": "Heslo" + }, + "title": "Znovu ov\u011b\u0159it integraci" + }, "trusted_device": { "data": { "trusted_device": "D\u016fv\u011bryhodn\u00e9 za\u0159\u00edzen\u00ed" diff --git a/homeassistant/components/icloud/translations/de.json b/homeassistant/components/icloud/translations/de.json index 207735018f0..4d9c0d63d0c 100644 --- a/homeassistant/components/icloud/translations/de.json +++ b/homeassistant/components/icloud/translations/de.json @@ -18,6 +18,13 @@ "description": "Dein zuvor eingegebenes Passwort f\u00fcr {username} funktioniert nicht mehr. Aktualisiere dein Passwort, um diese Integration weiterhin zu verwenden.", "title": "Integration erneut authentifizieren" }, + "reauth_confirm": { + "data": { + "password": "Passwort" + }, + "description": "Dein zuvor eingegebenes Passwort f\u00fcr {username} funktioniert nicht mehr. Aktualisiere dein Passwort, um diese Integration weiterhin zu verwenden.", + "title": "Integration erneut authentifizieren" + }, "trusted_device": { "data": { "trusted_device": "Vertrauensw\u00fcrdiges Ger\u00e4t" diff --git a/homeassistant/components/icloud/translations/el.json b/homeassistant/components/icloud/translations/el.json index cc484bd5660..d47a4349648 100644 --- a/homeassistant/components/icloud/translations/el.json +++ b/homeassistant/components/icloud/translations/el.json @@ -18,6 +18,13 @@ "description": "\u039f \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03c0\u03bf\u03c5 \u03b5\u03af\u03c7\u03b1\u03c4\u03b5 \u03b5\u03b9\u03c3\u03ac\u03b3\u03b5\u03b9 \u03c0\u03c1\u03bf\u03b7\u03b3\u03bf\u03c5\u03bc\u03ad\u03bd\u03c9\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03bf {username} \u03b4\u03b5\u03bd \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03b5\u03af \u03c0\u03bb\u03ad\u03bf\u03bd. \u0395\u03bd\u03b7\u03bc\u03b5\u03c1\u03ce\u03c3\u03c4\u03b5 \u03c4\u03bf\u03bd \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03ae\u03c2 \u03c3\u03b1\u03c2 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b5\u03c7\u03af\u03c3\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b5 \u03b1\u03c5\u03c4\u03ae \u03c4\u03b7\u03bd \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7.", "title": "\u0395\u03c0\u03b1\u03bd\u03b1\u03bb\u03b7\u03c0\u03c4\u03b9\u03ba\u03cc\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2" }, + "reauth_confirm": { + "data": { + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2" + }, + "description": "\u039f \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03c0\u03bf\u03c5 \u03b5\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b1\u03c4\u03b5 \u03c0\u03c1\u03bf\u03b7\u03b3\u03bf\u03c5\u03bc\u03ad\u03bd\u03c9\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03bf {username} \u03b4\u03b5\u03bd \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03b5\u03af \u03c0\u03bb\u03ad\u03bf\u03bd. \u0395\u03bd\u03b7\u03bc\u03b5\u03c1\u03ce\u03c3\u03c4\u03b5 \u03c4\u03bf\u03bd \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03ae\u03c2 \u03c3\u03b1\u03c2 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b5\u03c7\u03af\u03c3\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b5 \u03b1\u03c5\u03c4\u03ae\u03bd \u03c4\u03b7\u03bd \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7.", + "title": "\u0395\u03c0\u03b1\u03bd\u03b1\u03bb\u03b7\u03c0\u03c4\u03b9\u03ba\u03cc\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2" + }, "trusted_device": { "data": { "trusted_device": "\u0391\u03be\u03b9\u03cc\u03c0\u03b9\u03c3\u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae" diff --git a/homeassistant/components/icloud/translations/en.json b/homeassistant/components/icloud/translations/en.json index 65a8892c480..0052a858ede 100644 --- a/homeassistant/components/icloud/translations/en.json +++ b/homeassistant/components/icloud/translations/en.json @@ -11,6 +11,13 @@ "validate_verification_code": "Failed to verify your verification code, try again" }, "step": { + "reauth": { + "data": { + "password": "Password" + }, + "description": "Your previously entered password for {username} is no longer working. Update your password to keep using this integration.", + "title": "Reauthenticate Integration" + }, "reauth_confirm": { "data": { "password": "Password" diff --git a/homeassistant/components/icloud/translations/es.json b/homeassistant/components/icloud/translations/es.json index 5f0f4ac6495..ef1e6804469 100644 --- a/homeassistant/components/icloud/translations/es.json +++ b/homeassistant/components/icloud/translations/es.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", "no_device": "Ninguno de tus dispositivos tiene activado \"Buscar mi iPhone\"", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "error": { "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", @@ -18,6 +18,13 @@ "description": "La contrase\u00f1a introducida anteriormente para {username} ya no funciona. Actualiza tu contrase\u00f1a para seguir usando esta integraci\u00f3n.", "title": "Volver a autenticar la integraci\u00f3n" }, + "reauth_confirm": { + "data": { + "password": "Contrase\u00f1a" + }, + "description": "La contrase\u00f1a introducida anteriormente para {username} ya no funciona. Actualiza tu contrase\u00f1a para seguir usando esta integraci\u00f3n.", + "title": "Volver a autenticar la integraci\u00f3n" + }, "trusted_device": { "data": { "trusted_device": "Dispositivo de confianza" diff --git a/homeassistant/components/icloud/translations/et.json b/homeassistant/components/icloud/translations/et.json index af3457bb0db..686205f3572 100644 --- a/homeassistant/components/icloud/translations/et.json +++ b/homeassistant/components/icloud/translations/et.json @@ -18,6 +18,13 @@ "description": "Varem sisestatud salas\u00f5na kasutajale {username} ei t\u00f6\u00f6ta enam. Selle sidumise kasutamise j\u00e4tkamiseks v\u00e4rskenda oma salas\u00f5na.", "title": "iCloudi tuvastusandmed" }, + "reauth_confirm": { + "data": { + "password": "Salas\u00f5na" + }, + "description": "Varem sisestatud salas\u00f5na kasutajale {username} ei t\u00f6\u00f6ta enam. Selle sidumise kasutamise j\u00e4tkamiseks v\u00e4rskenda oma salas\u00f5na.", + "title": "Taastuvasta sidumine" + }, "trusted_device": { "data": { "trusted_device": "Usaldusv\u00e4\u00e4rne seade" diff --git a/homeassistant/components/icloud/translations/fr.json b/homeassistant/components/icloud/translations/fr.json index 8e5ec918cb0..dec1bbdb34a 100644 --- a/homeassistant/components/icloud/translations/fr.json +++ b/homeassistant/components/icloud/translations/fr.json @@ -18,6 +18,12 @@ "description": "Votre mot de passe pr\u00e9c\u00e9demment saisi pour {username} ne fonctionne plus. Mettez \u00e0 jour votre mot de passe pour continuer \u00e0 utiliser cette int\u00e9gration.", "title": "R\u00e9-authentifier l'int\u00e9gration" }, + "reauth_confirm": { + "data": { + "password": "Mot de passe" + }, + "title": "R\u00e9-authentifier l'int\u00e9gration" + }, "trusted_device": { "data": { "trusted_device": "Appareil de confiance" diff --git a/homeassistant/components/icloud/translations/hu.json b/homeassistant/components/icloud/translations/hu.json index 1cbbbfb6974..539b3740e24 100644 --- a/homeassistant/components/icloud/translations/hu.json +++ b/homeassistant/components/icloud/translations/hu.json @@ -18,6 +18,13 @@ "description": "{username} kor\u00e1bban megadott jelszava m\u00e1r nem m\u0171k\u00f6dik. Az integr\u00e1ci\u00f3 haszn\u00e1lat\u00e1hoz friss\u00edtse jelszav\u00e1t.", "title": "Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se" }, + "reauth_confirm": { + "data": { + "password": "Jelsz\u00f3" + }, + "description": "{username} kor\u00e1bban megadott jelszava m\u00e1r nem m\u0171k\u00f6dik. Az integr\u00e1ci\u00f3 haszn\u00e1lat\u00e1hoz friss\u00edtse jelszav\u00e1t.", + "title": "Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se" + }, "trusted_device": { "data": { "trusted_device": "Megb\u00edzhat\u00f3 eszk\u00f6z" diff --git a/homeassistant/components/icloud/translations/id.json b/homeassistant/components/icloud/translations/id.json index cd7abc1945d..1f6ed7c84c9 100644 --- a/homeassistant/components/icloud/translations/id.json +++ b/homeassistant/components/icloud/translations/id.json @@ -18,6 +18,13 @@ "description": "Kata sandi yang Anda masukkan sebelumnya untuk {username} tidak lagi berfungsi. Perbarui kata sandi Anda untuk tetap menggunakan integrasi ini.", "title": "Autentikasi Ulang Integrasi" }, + "reauth_confirm": { + "data": { + "password": "Kata Sandi" + }, + "description": "Kata sandi yang Anda masukkan sebelumnya untuk {username} tidak lagi berfungsi. Perbarui kata sandi Anda untuk tetap menggunakan integrasi ini.", + "title": "Autentikasi Ulang Integrasi" + }, "trusted_device": { "data": { "trusted_device": "Perangkat tepercaya" diff --git a/homeassistant/components/icloud/translations/it.json b/homeassistant/components/icloud/translations/it.json index 777498d6340..856ed30d767 100644 --- a/homeassistant/components/icloud/translations/it.json +++ b/homeassistant/components/icloud/translations/it.json @@ -18,6 +18,13 @@ "description": "La password inserita in precedenza per {username} non funziona pi\u00f9. Aggiorna la tua password per continuare a utilizzare questa integrazione.", "title": "Autentica nuovamente l'integrazione" }, + "reauth_confirm": { + "data": { + "password": "Password" + }, + "description": "La password precedentemente inserita per {username} non funziona pi\u00f9. Aggiorna la tua password per continuare a utilizzare questa integrazione.", + "title": "Autentica nuovamente l'integrazione" + }, "trusted_device": { "data": { "trusted_device": "Dispositivo attendibile" diff --git a/homeassistant/components/icloud/translations/ja.json b/homeassistant/components/icloud/translations/ja.json index 2295e72f0b2..4d9230ec150 100644 --- a/homeassistant/components/icloud/translations/ja.json +++ b/homeassistant/components/icloud/translations/ja.json @@ -18,6 +18,13 @@ "description": "\u4ee5\u524d\u306b\u5165\u529b\u3057\u305f {username} \u306e\u30d1\u30b9\u30ef\u30fc\u30c9\u306f\u4f7f\u3048\u306a\u304f\u306a\u308a\u307e\u3057\u305f\u3002\u3053\u306e\u7d71\u5408\u3092\u5f15\u304d\u7d9a\u304d\u4f7f\u7528\u3059\u308b\u306b\u306f\u3001\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u66f4\u65b0\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" }, + "reauth_confirm": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + }, + "description": "\u4ee5\u524d\u306b\u5165\u529b\u3057\u305f {username} \u306e\u30d1\u30b9\u30ef\u30fc\u30c9\u306f\u4f7f\u3048\u306a\u304f\u306a\u308a\u307e\u3057\u305f\u3002\u3053\u306e\u7d71\u5408\u3092\u5f15\u304d\u7d9a\u304d\u4f7f\u7528\u3059\u308b\u306b\u306f\u3001\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u66f4\u65b0\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" + }, "trusted_device": { "data": { "trusted_device": "\u4fe1\u983c\u3067\u304d\u308b\u30c7\u30d0\u30a4\u30b9" diff --git a/homeassistant/components/icloud/translations/nl.json b/homeassistant/components/icloud/translations/nl.json index 522df78a737..6c831c2f65b 100644 --- a/homeassistant/components/icloud/translations/nl.json +++ b/homeassistant/components/icloud/translations/nl.json @@ -18,6 +18,13 @@ "description": "Uw eerder ingevoerde wachtwoord voor {username} werkt niet meer. Update uw wachtwoord om deze integratie te blijven gebruiken.", "title": "Integratie herauthenticeren" }, + "reauth_confirm": { + "data": { + "password": "Wachtwoord" + }, + "description": "Je eerder ingevoerde wachtwoord voor {username} werkt niet meer. Update je wachtwoord om deze integratie te blijven gebruiken.", + "title": "Integratie herauthenticeren" + }, "trusted_device": { "data": { "trusted_device": "Vertrouwd apparaat" diff --git a/homeassistant/components/icloud/translations/no.json b/homeassistant/components/icloud/translations/no.json index 3e20aef032e..662600eac36 100644 --- a/homeassistant/components/icloud/translations/no.json +++ b/homeassistant/components/icloud/translations/no.json @@ -18,6 +18,13 @@ "description": "Ditt tidligere angitte passord for {username} fungerer ikke lenger. Oppdater passordet ditt for \u00e5 fortsette \u00e5 bruke denne integrasjonen.", "title": "Godkjenne integrering p\u00e5 nytt" }, + "reauth_confirm": { + "data": { + "password": "Passord" + }, + "description": "Det tidligere oppgitte passordet for {username} fungerer ikke lenger. Oppdater passordet ditt for \u00e5 fortsette \u00e5 bruke denne integrasjonen.", + "title": "Godkjenne integrering p\u00e5 nytt" + }, "trusted_device": { "data": { "trusted_device": "P\u00e5litelig enhet" diff --git a/homeassistant/components/icloud/translations/pl.json b/homeassistant/components/icloud/translations/pl.json index e111518710b..a726cd5a78d 100644 --- a/homeassistant/components/icloud/translations/pl.json +++ b/homeassistant/components/icloud/translations/pl.json @@ -18,6 +18,13 @@ "description": "Twoje poprzednio wprowadzone has\u0142o dla {username} ju\u017c nie dzia\u0142a. Zaktualizuj swoje has\u0142o, aby nadal korzysta\u0107 z tej integracji.", "title": "Ponownie uwierzytelnij integracj\u0119" }, + "reauth_confirm": { + "data": { + "password": "Has\u0142o" + }, + "description": "Twoje poprzednio wprowadzone has\u0142o dla {username} ju\u017c nie dzia\u0142a. Zaktualizuj swoje has\u0142o, aby nadal korzysta\u0107 z tej integracji.", + "title": "Ponownie uwierzytelnij integracj\u0119" + }, "trusted_device": { "data": { "trusted_device": "Zaufane urz\u0105dzenie" diff --git a/homeassistant/components/icloud/translations/pt-BR.json b/homeassistant/components/icloud/translations/pt-BR.json index 0c3363a3799..99f779e9ead 100644 --- a/homeassistant/components/icloud/translations/pt-BR.json +++ b/homeassistant/components/icloud/translations/pt-BR.json @@ -18,6 +18,13 @@ "description": "Sua senha inserida anteriormente para {username} n\u00e3o est\u00e1 mais funcionando. Atualize sua senha para continuar usando esta integra\u00e7\u00e3o.", "title": "Reautenticar Integra\u00e7\u00e3o" }, + "reauth_confirm": { + "data": { + "password": "Senha" + }, + "description": "Sua senha inserida anteriormente para {username} n\u00e3o est\u00e1 mais funcionando. Atualize sua senha para continuar usando esta integra\u00e7\u00e3o.", + "title": "Reautenticar Integra\u00e7\u00e3o" + }, "trusted_device": { "data": { "trusted_device": "Dispositivo confi\u00e1vel" diff --git a/homeassistant/components/icloud/translations/pt.json b/homeassistant/components/icloud/translations/pt.json index 3e8e4cce2b8..da7711298fc 100644 --- a/homeassistant/components/icloud/translations/pt.json +++ b/homeassistant/components/icloud/translations/pt.json @@ -15,6 +15,9 @@ "description": "A sua palavra-passe anteriormente introduzida para {username} j\u00e1 n\u00e3o \u00e9 v\u00e1lida. Atualize sua palavra-passe para continuar a utilizar esta integra\u00e7\u00e3o.", "title": "Reautenticar integra\u00e7\u00e3o" }, + "reauth_confirm": { + "description": "Sua senha inserida anteriormente para {username} n\u00e3o est\u00e1 mais funcionando. Atualize sua senha para continuar usando esta integra\u00e7\u00e3o." + }, "trusted_device": { "data": { "trusted_device": "Dispositivo confi\u00e1vel" diff --git a/homeassistant/components/icloud/translations/ru.json b/homeassistant/components/icloud/translations/ru.json index f3f85630215..17acded5703 100644 --- a/homeassistant/components/icloud/translations/ru.json +++ b/homeassistant/components/icloud/translations/ru.json @@ -18,6 +18,13 @@ "description": "\u0420\u0430\u043d\u0435\u0435 \u0432\u0432\u0435\u0434\u0435\u043d\u043d\u044b\u0439 \u043f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f {username} \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435 \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442. \u0410\u0432\u0442\u043e\u0440\u0438\u0437\u0443\u0439\u0442\u0435\u0441\u044c, \u0447\u0442\u043e\u0431\u044b \u043f\u0440\u043e\u0434\u043e\u043b\u0436\u0438\u0442\u044c \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u044d\u0442\u0443 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044e.", "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" }, + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + }, + "description": "\u0420\u0430\u043d\u0435\u0435 \u0432\u0432\u0435\u0434\u0435\u043d\u043d\u044b\u0439 \u043f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f {username} \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435 \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442. \u0410\u0432\u0442\u043e\u0440\u0438\u0437\u0443\u0439\u0442\u0435\u0441\u044c, \u0447\u0442\u043e\u0431\u044b \u043f\u0440\u043e\u0434\u043e\u043b\u0436\u0438\u0442\u044c \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u044d\u0442\u0443 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044e.", + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" + }, "trusted_device": { "data": { "trusted_device": "\u0414\u043e\u0432\u0435\u0440\u0435\u043d\u043d\u043e\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" diff --git a/homeassistant/components/icloud/translations/sv.json b/homeassistant/components/icloud/translations/sv.json index bb07cc8291e..b76a5408319 100644 --- a/homeassistant/components/icloud/translations/sv.json +++ b/homeassistant/components/icloud/translations/sv.json @@ -18,6 +18,13 @@ "description": "Ditt tidigare angivna l\u00f6senord f\u00f6r {username} fungerar inte l\u00e4ngre. Uppdatera ditt l\u00f6senord f\u00f6r att forts\u00e4tta anv\u00e4nda denna integration.", "title": "\u00c5terautenticera integration" }, + "reauth_confirm": { + "data": { + "password": "L\u00f6senord" + }, + "description": "Ditt tidigare angivna l\u00f6senord f\u00f6r {username} fungerar inte l\u00e4ngre. Uppdatera ditt l\u00f6senord f\u00f6r att forts\u00e4tta anv\u00e4nda denna integration.", + "title": "\u00c5terautenticera integration" + }, "trusted_device": { "data": { "trusted_device": "Betrodd enhet" diff --git a/homeassistant/components/icloud/translations/tr.json b/homeassistant/components/icloud/translations/tr.json index 0f917b132f4..e220141f24d 100644 --- a/homeassistant/components/icloud/translations/tr.json +++ b/homeassistant/components/icloud/translations/tr.json @@ -18,6 +18,13 @@ "description": "{username} i\u00e7in \u00f6nceden girdi\u011finiz \u015fifreniz art\u0131k \u00e7al\u0131\u015fm\u0131yor. Bu entegrasyonu kullanmaya devam etmek i\u00e7in \u015fifrenizi g\u00fcncelleyin.", "title": "Entegrasyonu Yeniden Do\u011frula" }, + "reauth_confirm": { + "data": { + "password": "Parola" + }, + "description": "{username} i\u00e7in \u00f6nceden girdi\u011finiz \u015fifre art\u0131k \u00e7al\u0131\u015fm\u0131yor. Bu entegrasyonu kullanmaya devam etmek i\u00e7in \u015fifrenizi g\u00fcncelleyin.", + "title": "Entegrasyonu Yeniden Do\u011frula" + }, "trusted_device": { "data": { "trusted_device": "G\u00fcvenilir ayg\u0131t" diff --git a/homeassistant/components/icloud/translations/zh-Hant.json b/homeassistant/components/icloud/translations/zh-Hant.json index fe421275e2a..91f14636dd2 100644 --- a/homeassistant/components/icloud/translations/zh-Hant.json +++ b/homeassistant/components/icloud/translations/zh-Hant.json @@ -18,6 +18,13 @@ "description": "\u5148\u524d\u91dd\u5c0d\u5e33\u865f {username} \u6240\u8f38\u5165\u7684\u5bc6\u78bc\u5df2\u5931\u6548\u3002\u8acb\u66f4\u65b0\u5bc6\u78bc\u4ee5\u4f7f\u7528\u6b64\u6574\u5408\u3002", "title": "\u91cd\u65b0\u8a8d\u8b49\u6574\u5408" }, + "reauth_confirm": { + "data": { + "password": "\u5bc6\u78bc" + }, + "description": "\u5148\u524d\u91dd\u5c0d\u5e33\u865f {username} \u6240\u8f38\u5165\u7684\u5bc6\u78bc\u5df2\u5931\u6548\u3002\u8acb\u66f4\u65b0\u5bc6\u78bc\u4ee5\u4f7f\u7528\u6b64\u6574\u5408\u3002", + "title": "\u91cd\u65b0\u8a8d\u8b49\u6574\u5408" + }, "trusted_device": { "data": { "trusted_device": "\u4fe1\u4efb\u88dd\u7f6e" diff --git a/homeassistant/components/ifttt/translations/pt.json b/homeassistant/components/ifttt/translations/pt.json index 030af8e090b..b99c81b0739 100644 --- a/homeassistant/components/ifttt/translations/pt.json +++ b/homeassistant/components/ifttt/translations/pt.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "N\u00e3o ligado ao Home Assistant Cloud.", "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel.", "webhook_not_internet_accessible": "O seu Home Assistant necessita estar acess\u00edvel a partir da Internet para receber mensagens do tipo webhook." }, diff --git a/homeassistant/components/image/manifest.json b/homeassistant/components/image/manifest.json index 4f967dbcc89..ed500c89011 100644 --- a/homeassistant/components/image/manifest.json +++ b/homeassistant/components/image/manifest.json @@ -6,5 +6,6 @@ "requirements": ["pillow==9.2.0"], "dependencies": ["http"], "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "system" } diff --git a/homeassistant/components/image_processing/__init__.py b/homeassistant/components/image_processing/__init__.py index 5c932826197..29adafe90b8 100644 --- a/homeassistant/components/image_processing/__init__.py +++ b/homeassistant/components/image_processing/__init__.py @@ -1,11 +1,15 @@ """Provides functionality to interact with image processing services.""" +from __future__ import annotations + import asyncio from datetime import timedelta import logging -from typing import final +from typing import Any, Final, TypedDict, final import voluptuous as vol +from homeassistant.backports.enum import StrEnum +from homeassistant.components.camera import Image from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_NAME, @@ -22,29 +26,35 @@ from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.typing import ConfigType from homeassistant.util.async_ import run_callback_threadsafe -# mypy: allow-untyped-defs, no-check-untyped-defs - _LOGGER = logging.getLogger(__name__) DOMAIN = "image_processing" SCAN_INTERVAL = timedelta(seconds=10) -DEVICE_CLASSES = [ - "alpr", # Automatic license plate recognition - "face", # Face - "ocr", # OCR -] + +class ImageProcessingDeviceClass(StrEnum): + """Device class for image processing entities.""" + + # Automatic license plate recognition + ALPR = "alpr" + + # Face + FACE = "face" + + # OCR + OCR = "ocr" + SERVICE_SCAN = "scan" EVENT_DETECT_FACE = "image_processing.detect_face" ATTR_AGE = "age" -ATTR_CONFIDENCE = "confidence" +ATTR_CONFIDENCE: Final = "confidence" ATTR_FACES = "faces" ATTR_GENDER = "gender" ATTR_GLASSES = "glasses" -ATTR_MOTION = "motion" +ATTR_MOTION: Final = "motion" ATTR_TOTAL_FACES = "total_faces" CONF_CONFIDENCE = "confidence" @@ -70,9 +80,23 @@ PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend( PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE.extend(PLATFORM_SCHEMA.schema) +class FaceInformation(TypedDict, total=False): + """Face information.""" + + confidence: float + name: str + age: float + gender: str + motion: str + glasses: str + entity_id: str + + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the image processing.""" - component = EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL) + component = EntityComponent[ImageProcessingEntity]( + _LOGGER, DOMAIN, hass, SCAN_INTERVAL + ) await component.async_setup(config) @@ -98,36 +122,36 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: class ImageProcessingEntity(Entity): """Base entity class for image processing.""" + _attr_device_class: ImageProcessingDeviceClass | str | None timeout = DEFAULT_TIMEOUT @property - def camera_entity(self): + def camera_entity(self) -> str | None: """Return camera entity id from process pictures.""" return None @property - def confidence(self): + def confidence(self) -> float | None: """Return minimum confidence for do some things.""" return None - def process_image(self, image): + def process_image(self, image: bytes) -> None: """Process image.""" raise NotImplementedError() - async def async_process_image(self, image): + async def async_process_image(self, image: bytes) -> None: """Process image.""" return await self.hass.async_add_executor_job(self.process_image, image) - async def async_update(self): + async def async_update(self) -> None: """Update image and process it. This method is a coroutine. """ camera = self.hass.components.camera - image = None try: - image = await camera.async_get_image( + image: Image = await camera.async_get_image( self.camera_entity, timeout=self.timeout ) @@ -142,15 +166,17 @@ class ImageProcessingEntity(Entity): class ImageProcessingFaceEntity(ImageProcessingEntity): """Base entity class for face image processing.""" - def __init__(self): + _attr_device_class = ImageProcessingDeviceClass.FACE + + def __init__(self) -> None: """Initialize base face identify/verify entity.""" - self.faces = [] + self.faces: list[FaceInformation] = [] self.total_faces = 0 @property - def state(self): + def state(self) -> str | int | None: """Return the state of the entity.""" - confidence = 0 + confidence: float = 0 state = None # No confidence support @@ -166,30 +192,25 @@ class ImageProcessingFaceEntity(ImageProcessingEntity): confidence = f_co for attr in (ATTR_NAME, ATTR_MOTION): if attr in face: - state = face[attr] + state = face[attr] # type: ignore[literal-required] break return state - @property - def device_class(self): - """Return the class of this device, from component DEVICE_CLASSES.""" - return "face" - @final @property - def state_attributes(self): + def state_attributes(self) -> dict[str, Any]: """Return device specific state attributes.""" return {ATTR_FACES: self.faces, ATTR_TOTAL_FACES: self.total_faces} - def process_faces(self, faces, total): + def process_faces(self, faces: list[FaceInformation], total: int) -> None: """Send event with detected faces and store data.""" run_callback_threadsafe( self.hass.loop, self.async_process_faces, faces, total ).result() @callback - def async_process_faces(self, faces, total): + def async_process_faces(self, faces: list[FaceInformation], total: int) -> None: """Send event with detected faces and store data. known are a dict in follow format: diff --git a/homeassistant/components/image_processing/manifest.json b/homeassistant/components/image_processing/manifest.json index 0315a69b82a..43a52268881 100644 --- a/homeassistant/components/image_processing/manifest.json +++ b/homeassistant/components/image_processing/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/image_processing", "dependencies": ["camera"], "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "entity" } diff --git a/homeassistant/components/incomfort/climate.py b/homeassistant/components/incomfort/climate.py index b7b66e2b25d..1d3c18fa608 100644 --- a/homeassistant/components/incomfort/climate.py +++ b/homeassistant/components/incomfort/climate.py @@ -3,8 +3,12 @@ from __future__ import annotations from typing import Any -from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN, ClimateEntity -from homeassistant.components.climate.const import ClimateEntityFeature, HVACMode +from homeassistant.components.climate import ( + DOMAIN as CLIMATE_DOMAIN, + ClimateEntity, + ClimateEntityFeature, + HVACMode, +) from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback diff --git a/homeassistant/components/influxdb/__init__.py b/homeassistant/components/influxdb/__init__.py index 72871c75fc4..4fd6eb58fdd 100644 --- a/homeassistant/components/influxdb/__init__.py +++ b/homeassistant/components/influxdb/__init__.py @@ -218,7 +218,7 @@ def _generate_event_to_json(conf: dict) -> Callable[[Event], dict[str, Any] | No state: State | None = event.data.get(EVENT_NEW_STATE) if ( state is None - or state.state in (STATE_UNKNOWN, "", STATE_UNAVAILABLE) + or state.state in (STATE_UNKNOWN, "", STATE_UNAVAILABLE, None) or not entity_filter(state.entity_id) ): return None diff --git a/homeassistant/components/inkbird/translations/bg.json b/homeassistant/components/inkbird/translations/bg.json new file mode 100644 index 00000000000..a61dac839ad --- /dev/null +++ b/homeassistant/components/inkbird/translations/bg.json @@ -0,0 +1,20 @@ +{ + "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" + }, + "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/inkbird/translations/cs.json b/homeassistant/components/inkbird/translations/cs.json new file mode 100644 index 00000000000..925c1cbd337 --- /dev/null +++ b/homeassistant/components/inkbird/translations/cs.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "already_in_progress": "Konfigurace ji\u017e prob\u00edh\u00e1", + "no_devices_found": "V s\u00edti nebyla nalezena \u017e\u00e1dn\u00e1 za\u0159\u00edzen\u00ed" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Chcete nastavit {name}?" + }, + "user": { + "data": { + "address": "Za\u0159\u00edzen\u00ed" + }, + "description": "Zvolte za\u0159\u00edzen\u00ed, kter\u00e9 chcete nastavit" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/input_boolean/__init__.py b/homeassistant/components/input_boolean/__init__.py index a43b132a0e2..d1d19247121 100644 --- a/homeassistant/components/input_boolean/__init__.py +++ b/homeassistant/components/input_boolean/__init__.py @@ -92,7 +92,7 @@ def is_on(hass: HomeAssistant, entity_id: str) -> bool: async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up an input boolean.""" - component = EntityComponent(_LOGGER, DOMAIN, hass) + component = EntityComponent[InputBoolean](_LOGGER, DOMAIN, hass) # Process integration platforms right away since # we will create entities before firing EVENT_COMPONENT_LOADED @@ -104,7 +104,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: logging.getLogger(f"{__name__}.yaml_collection"), id_manager ) collection.sync_entity_lifecycle( - hass, DOMAIN, DOMAIN, component, yaml_collection, InputBoolean.from_yaml + hass, DOMAIN, DOMAIN, component, yaml_collection, InputBoolean ) storage_collection = InputBooleanStorageCollection( @@ -154,21 +154,28 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return True -class InputBoolean(ToggleEntity, RestoreEntity): +class InputBoolean(collection.CollectionEntity, ToggleEntity, RestoreEntity): """Representation of a boolean input.""" _attr_should_poll = False + editable: bool def __init__(self, config: ConfigType) -> None: """Initialize a boolean input.""" self._config = config - self.editable = True self._attr_is_on = config.get(CONF_INITIAL, False) self._attr_unique_id = config[CONF_ID] + @classmethod + def from_storage(cls, config: ConfigType) -> InputBoolean: + """Return entity instance initialized from storage.""" + input_bool = cls(config) + input_bool.editable = True + return input_bool + @classmethod def from_yaml(cls, config: ConfigType) -> InputBoolean: - """Return entity instance initialized from yaml storage.""" + """Return entity instance initialized from yaml.""" input_bool = cls(config) input_bool.entity_id = f"{DOMAIN}.{config[CONF_ID]}" input_bool.editable = False diff --git a/homeassistant/components/input_button/__init__.py b/homeassistant/components/input_button/__init__.py index a47e96d635d..f425c8e3da2 100644 --- a/homeassistant/components/input_button/__init__.py +++ b/homeassistant/components/input_button/__init__.py @@ -77,7 +77,7 @@ class InputButtonStorageCollection(collection.StorageCollection): async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up an input button.""" - component = EntityComponent(_LOGGER, DOMAIN, hass) + component = EntityComponent[InputButton](_LOGGER, DOMAIN, hass) # Process integration platforms right away since # we will create entities before firing EVENT_COMPONENT_LOADED @@ -89,7 +89,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: logging.getLogger(f"{__name__}.yaml_collection"), id_manager ) collection.sync_entity_lifecycle( - hass, DOMAIN, DOMAIN, component, yaml_collection, InputButton.from_yaml + hass, DOMAIN, DOMAIN, component, yaml_collection, InputButton ) storage_collection = InputButtonStorageCollection( @@ -135,20 +135,27 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return True -class InputButton(ButtonEntity, RestoreEntity): +class InputButton(collection.CollectionEntity, ButtonEntity, RestoreEntity): """Representation of a button.""" _attr_should_poll = False + editable: bool def __init__(self, config: ConfigType) -> None: """Initialize a button.""" self._config = config - self.editable = True self._attr_unique_id = config[CONF_ID] @classmethod - def from_yaml(cls, config: ConfigType) -> ButtonEntity: - """Return entity instance initialized from yaml storage.""" + def from_storage(cls, config: ConfigType) -> InputButton: + """Return entity instance initialized from storage.""" + button = cls(config) + button.editable = True + return button + + @classmethod + def from_yaml(cls, config: ConfigType) -> InputButton: + """Return entity instance initialized from yaml.""" button = cls(config) button.entity_id = f"{DOMAIN}.{config[CONF_ID]}" button.editable = False diff --git a/homeassistant/components/input_datetime/__init__.py b/homeassistant/components/input_datetime/__init__.py index daedfd251b0..5913789d53f 100644 --- a/homeassistant/components/input_datetime/__init__.py +++ b/homeassistant/components/input_datetime/__init__.py @@ -130,7 +130,7 @@ RELOAD_SERVICE_SCHEMA = vol.Schema({}) async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up an input datetime.""" - component = EntityComponent(_LOGGER, DOMAIN, hass) + component = EntityComponent[InputDatetime](_LOGGER, DOMAIN, hass) # Process integration platforms right away since # we will create entities before firing EVENT_COMPONENT_LOADED @@ -142,7 +142,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: logging.getLogger(f"{__name__}.yaml_collection"), id_manager ) collection.sync_entity_lifecycle( - hass, DOMAIN, DOMAIN, component, yaml_collection, InputDatetime.from_yaml + hass, DOMAIN, DOMAIN, component, yaml_collection, InputDatetime ) storage_collection = DateTimeStorageCollection( @@ -223,15 +223,15 @@ class DateTimeStorageCollection(collection.StorageCollection): return {CONF_ID: data[CONF_ID]} | update_data -class InputDatetime(RestoreEntity): +class InputDatetime(collection.CollectionEntity, RestoreEntity): """Representation of a datetime input.""" _attr_should_poll = False + editable: bool - def __init__(self, config: dict) -> None: + def __init__(self, config: ConfigType) -> None: """Initialize a select input.""" self._config = config - self.editable = True self._current_datetime = None if not config.get(CONF_INITIAL): @@ -250,8 +250,15 @@ class InputDatetime(RestoreEntity): ) @classmethod - def from_yaml(cls, config: dict) -> InputDatetime: - """Return entity instance initialized from yaml storage.""" + def from_storage(cls, config: ConfigType) -> InputDatetime: + """Return entity instance initialized from storage.""" + input_dt = cls(config) + input_dt.editable = True + return input_dt + + @classmethod + def from_yaml(cls, config: ConfigType) -> InputDatetime: + """Return entity instance initialized from yaml.""" input_dt = cls(config) input_dt.entity_id = f"{DOMAIN}.{config[CONF_ID]}" input_dt.editable = False @@ -412,7 +419,7 @@ class InputDatetime(RestoreEntity): ) self.async_write_ha_state() - async def async_update_config(self, config: dict) -> None: + async def async_update_config(self, config: ConfigType) -> None: """Handle when the config is updated.""" self._config = config self.async_write_ha_state() diff --git a/homeassistant/components/input_number/__init__.py b/homeassistant/components/input_number/__init__.py index 00aeca2ab34..99e54dc9baa 100644 --- a/homeassistant/components/input_number/__init__.py +++ b/homeassistant/components/input_number/__init__.py @@ -107,7 +107,7 @@ STORAGE_VERSION = 1 async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up an input slider.""" - component = EntityComponent(_LOGGER, DOMAIN, hass) + component = EntityComponent[InputNumber](_LOGGER, DOMAIN, hass) # Process integration platforms right away since # we will create entities before firing EVENT_COMPONENT_LOADED @@ -119,7 +119,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: logging.getLogger(f"{__name__}.yaml_collection"), id_manager ) collection.sync_entity_lifecycle( - hass, DOMAIN, DOMAIN, component, yaml_collection, InputNumber.from_yaml + hass, DOMAIN, DOMAIN, component, yaml_collection, InputNumber ) storage_collection = NumberStorageCollection( @@ -206,20 +206,27 @@ class NumberStorageCollection(collection.StorageCollection): return {CONF_ID: data[CONF_ID]} | update_data -class InputNumber(RestoreEntity): +class InputNumber(collection.CollectionEntity, RestoreEntity): """Representation of a slider.""" _attr_should_poll = False + editable: bool - def __init__(self, config: dict) -> None: + def __init__(self, config: ConfigType) -> None: """Initialize an input number.""" self._config = config - self.editable = True self._current_value: float | None = config.get(CONF_INITIAL) @classmethod - def from_yaml(cls, config: dict) -> InputNumber: - """Return entity instance initialized from yaml storage.""" + def from_storage(cls, config: ConfigType) -> InputNumber: + """Return entity instance initialized from storage.""" + input_num = cls(config) + input_num.editable = True + return input_num + + @classmethod + def from_yaml(cls, config: ConfigType) -> InputNumber: + """Return entity instance initialized from yaml.""" input_num = cls(config) input_num.entity_id = f"{DOMAIN}.{config[CONF_ID]}" input_num.editable = False @@ -314,7 +321,7 @@ class InputNumber(RestoreEntity): """Decrement value.""" await self.async_set_value(max(self._current_value - self._step, self._minimum)) - async def async_update_config(self, config: dict) -> None: + async def async_update_config(self, config: ConfigType) -> None: """Handle when the config is updated.""" self._config = config # just in case min/max values changed diff --git a/homeassistant/components/input_select/__init__.py b/homeassistant/components/input_select/__init__.py index 5d0e356b8ee..f30b2ca1e36 100644 --- a/homeassistant/components/input_select/__init__.py +++ b/homeassistant/components/input_select/__init__.py @@ -132,7 +132,7 @@ class InputSelectStore(Store): async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up an input select.""" - component = EntityComponent(_LOGGER, DOMAIN, hass) + component = EntityComponent[InputSelect](_LOGGER, DOMAIN, hass) # Process integration platforms right away since # we will create entities before firing EVENT_COMPONENT_LOADED @@ -144,7 +144,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: logging.getLogger(f"{__name__}.yaml_collection"), id_manager ) collection.sync_entity_lifecycle( - hass, DOMAIN, DOMAIN, component, yaml_collection, InputSelect.from_yaml + hass, DOMAIN, DOMAIN, component, yaml_collection, InputSelect ) storage_collection = InputSelectStorageCollection( @@ -249,11 +249,11 @@ class InputSelectStorageCollection(collection.StorageCollection): return {CONF_ID: data[CONF_ID]} | update_data -class InputSelect(SelectEntity, RestoreEntity): +class InputSelect(collection.CollectionEntity, SelectEntity, RestoreEntity): """Representation of a select input.""" _attr_should_poll = False - editable = True + editable: bool def __init__(self, config: ConfigType) -> None: """Initialize a select input.""" @@ -263,9 +263,16 @@ class InputSelect(SelectEntity, RestoreEntity): self._attr_options = config[CONF_OPTIONS] self._attr_unique_id = config[CONF_ID] + @classmethod + def from_storage(cls, config: ConfigType) -> InputSelect: + """Return entity instance initialized from storage.""" + input_select = cls(config) + input_select.editable = True + return input_select + @classmethod def from_yaml(cls, config: ConfigType) -> InputSelect: - """Return entity instance initialized from yaml storage.""" + """Return entity instance initialized from yaml.""" input_select = cls(config) input_select.entity_id = f"{DOMAIN}.{config[CONF_ID]}" input_select.editable = False diff --git a/homeassistant/components/input_text/__init__.py b/homeassistant/components/input_text/__init__.py index 211d9843996..6069ae8143a 100644 --- a/homeassistant/components/input_text/__init__.py +++ b/homeassistant/components/input_text/__init__.py @@ -107,7 +107,7 @@ RELOAD_SERVICE_SCHEMA = vol.Schema({}) async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up an input text.""" - component = EntityComponent(_LOGGER, DOMAIN, hass) + component = EntityComponent[InputText](_LOGGER, DOMAIN, hass) # Process integration platforms right away since # we will create entities before firing EVENT_COMPONENT_LOADED @@ -119,7 +119,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: logging.getLogger(f"{__name__}.yaml_collection"), id_manager ) collection.sync_entity_lifecycle( - hass, DOMAIN, DOMAIN, component, yaml_collection, InputText.from_yaml + hass, DOMAIN, DOMAIN, component, yaml_collection, InputText ) storage_collection = InputTextStorageCollection( @@ -184,20 +184,27 @@ class InputTextStorageCollection(collection.StorageCollection): return {CONF_ID: data[CONF_ID]} | update_data -class InputText(RestoreEntity): +class InputText(collection.CollectionEntity, RestoreEntity): """Represent a text box.""" _attr_should_poll = False + editable: bool - def __init__(self, config: dict) -> None: + def __init__(self, config: ConfigType) -> None: """Initialize a text input.""" self._config = config - self.editable = True self._current_value = config.get(CONF_INITIAL) @classmethod - def from_yaml(cls, config: dict) -> InputText: - """Return entity instance initialized from yaml storage.""" + def from_storage(cls, config: ConfigType) -> InputText: + """Return entity instance initialized from storage.""" + input_text = cls(config) + input_text.editable = True + return input_text + + @classmethod + def from_yaml(cls, config: ConfigType) -> InputText: + """Return entity instance initialized from yaml.""" input_text = cls(config) input_text.entity_id = f"{DOMAIN}.{config[CONF_ID]}" input_text.editable = False @@ -275,7 +282,7 @@ class InputText(RestoreEntity): self._current_value = value self.async_write_ha_state() - async def async_update_config(self, config: dict) -> None: + async def async_update_config(self, config: ConfigType) -> None: """Handle when the config is updated.""" self._config = config self.async_write_ha_state() diff --git a/homeassistant/components/insteon/api/__init__.py b/homeassistant/components/insteon/api/__init__.py index 71dd1a0463e..e56d4dab07e 100644 --- a/homeassistant/components/insteon/api/__init__.py +++ b/homeassistant/components/insteon/api/__init__.py @@ -3,9 +3,9 @@ from insteon_frontend import get_build_id, locate_dir from homeassistant.components import panel_custom, websocket_api -from homeassistant.components.insteon.const import CONF_DEV_PATH, DOMAIN from homeassistant.core import HomeAssistant, callback +from ..const import CONF_DEV_PATH, DOMAIN from .aldb import ( websocket_add_default_links, websocket_change_aldb_record, diff --git a/homeassistant/components/insteon/climate.py b/homeassistant/components/insteon/climate.py index 922ef141350..0211b316b96 100644 --- a/homeassistant/components/insteon/climate.py +++ b/homeassistant/components/insteon/climate.py @@ -6,12 +6,12 @@ from typing import Any from pyinsteon.config import CELSIUS from pyinsteon.constants import ThermostatMode -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, DOMAIN as CLIMATE_DOMAIN, FAN_AUTO, + ClimateEntity, ClimateEntityFeature, HVACAction, HVACMode, diff --git a/homeassistant/components/insteon/translations/cs.json b/homeassistant/components/insteon/translations/cs.json index e10e25e6361..a80e657efef 100644 --- a/homeassistant/components/insteon/translations/cs.json +++ b/homeassistant/components/insteon/translations/cs.json @@ -9,6 +9,9 @@ "select_single": "Vyberte jednu mo\u017enost." }, "step": { + "confirm_usb": { + "description": "Chcete nastavit {name}?" + }, "hubv1": { "data": { "host": "IP adresa", diff --git a/homeassistant/components/integration/sensor.py b/homeassistant/components/integration/sensor.py index b1b666af9aa..d08d04e094d 100644 --- a/homeassistant/components/integration/sensor.py +++ b/homeassistant/components/integration/sensor.py @@ -3,6 +3,7 @@ from __future__ import annotations from decimal import Decimal, DecimalException import logging +from typing import Final import voluptuous as vol @@ -26,7 +27,7 @@ from homeassistant.const import ( TIME_MINUTES, TIME_SECONDS, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import Event, HomeAssistant, State, callback from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_state_change_event @@ -45,11 +46,9 @@ from .const import ( METHOD_TRAPEZOIDAL, ) -# mypy: allow-untyped-defs, no-check-untyped-defs - _LOGGER = logging.getLogger(__name__) -ATTR_SOURCE_ID = "source" +ATTR_SOURCE_ID: Final = "source" # SI Metric prefixes UNIT_PREFIXES = {None: 1, "k": 10**3, "M": 10**6, "G": 10**9, "T": 10**12} @@ -135,6 +134,9 @@ async def async_setup_platform( class IntegrationSensor(RestoreEntity, SensorEntity): """Representation of an integration sensor.""" + _attr_state_class = SensorStateClass.TOTAL + _attr_should_poll = False + def __init__( self, *, @@ -155,13 +157,11 @@ class IntegrationSensor(RestoreEntity, SensorEntity): self._attr_name = name if name is not None else f"{source_entity} integral" self._unit_template = f"{'' if unit_prefix is None else unit_prefix}{{}}" - self._unit_of_measurement = None + self._unit_of_measurement: str | None = None self._unit_prefix = UNIT_PREFIXES[unit_prefix] self._unit_time = UNIT_TIME[unit_time] self._unit_time_str = unit_time - self._attr_state_class = SensorStateClass.TOTAL self._attr_icon = "mdi:chart-histogram" - self._attr_should_poll = False self._attr_extra_state_attributes = {ATTR_SOURCE_ID: source_entity} def _unit(self, source_unit: str) -> str: @@ -195,10 +195,10 @@ class IntegrationSensor(RestoreEntity, SensorEntity): ) @callback - def calc_integration(event): + def calc_integration(event: Event) -> None: """Handle the sensor state changes.""" - old_state = event.data.get("old_state") - new_state = event.data.get("new_state") + old_state: State | None = event.data.get("old_state") + new_state: State | None = event.data.get("new_state") if new_state is None or new_state.state in ( STATE_UNKNOWN, @@ -237,7 +237,7 @@ class IntegrationSensor(RestoreEntity, SensorEntity): try: # integration as the Riemann integral of previous measures. - area = 0 + area = Decimal(0) elapsed_time = ( new_state.last_updated - old_state.last_updated ).total_seconds() @@ -277,13 +277,13 @@ class IntegrationSensor(RestoreEntity, SensorEntity): ) @property - def native_value(self): + def native_value(self) -> Decimal | None: """Return the state of the sensor.""" if isinstance(self._state, Decimal): return round(self._state, self._round_digits) return self._state @property - def native_unit_of_measurement(self): + def native_unit_of_measurement(self) -> str | None: """Return the unit the value is expressed in.""" return self._unit_of_measurement diff --git a/homeassistant/components/integration/translations/it.json b/homeassistant/components/integration/translations/it.json index 92e4077aa90..ae7e110280d 100644 --- a/homeassistant/components/integration/translations/it.json +++ b/homeassistant/components/integration/translations/it.json @@ -32,5 +32,5 @@ } } }, - "title": "Integrazione - Sensore integrale di somma Riemann" + "title": "Integrazione - Sensore integrale a somma di Riemann" } \ No newline at end of file diff --git a/homeassistant/components/integration/translations/ja.json b/homeassistant/components/integration/translations/ja.json index 237d7eef353..011183a820a 100644 --- a/homeassistant/components/integration/translations/ja.json +++ b/homeassistant/components/integration/translations/ja.json @@ -8,15 +8,15 @@ "round": "\u7cbe\u5ea6", "source": "\u5165\u529b\u30bb\u30f3\u30b5\u30fc", "unit_prefix": "\u30e1\u30c8\u30ea\u30c3\u30af\u63a5\u982d\u8f9e", - "unit_time": "\u7a4d\u7b97\u6642\u9593" + "unit_time": "\u6642\u9593\u5358\u4f4d" }, "data_description": { "round": "\u51fa\u529b\u5024\u306e\u5c0f\u6570\u70b9\u4ee5\u4e0b\u306e\u6841\u6570\u3002", "unit_prefix": "\u51fa\u529b\u306f\u3001\u9078\u629e\u3055\u308c\u305f\u5358\u4f4d\u306e\u30d7\u30ec\u30d5\u30a3\u30c3\u30af\u30b9\u306b\u5f93\u3063\u3066\u30b9\u30b1\u30fc\u30ea\u30f3\u30b0\u3055\u308c\u307e\u3059\u3002", "unit_time": "\u51fa\u529b\u306f\u3001\u9078\u629e\u3055\u308c\u305f\u6642\u9593\u5358\u4f4d\u306b\u5f93\u3063\u3066\u30b9\u30b1\u30fc\u30ea\u30f3\u30b0\u3055\u308c\u307e\u3059\u3002" }, - "description": "\u7cbe\u5ea6\u306f\u3001\u51fa\u529b\u306e\u5c0f\u6570\u70b9\u4ee5\u4e0b\u306e\u6841\u6570\u3092\u5236\u5fa1\u3057\u307e\u3059\u3002\n\u5408\u8a08\u306f\u3001\u9078\u629e\u3055\u308c\u305f\u5358\u4f4d\u306e\u30d7\u30ea\u30d5\u30a3\u30c3\u30af\u30b9\u3068\u7a4d\u5206\u6642\u9593\u306b\u5f93\u3063\u3066\u30b9\u30b1\u30fc\u30ea\u30f3\u30b0\u3055\u308c\u307e\u3059\u3002", - "title": "\u65b0\u3057\u3044\u7d71\u5408\u30bb\u30f3\u30b5\u30fc" + "description": "\u30ea\u30fc\u30de\u30f3\u548c\u3092\u8a08\u7b97\u3057\u3066\u30bb\u30f3\u30b5\u30fc\u306e\u7a4d\u5206\u3092\u63a8\u5b9a\u3059\u308b\u30bb\u30f3\u30b5\u30fc\u3092\u4f5c\u6210\u3057\u307e\u3059\u3002", + "title": "\u30ea\u30fc\u30de\u30f3\u548c\u7a4d\u5206\u30bb\u30f3\u30b5\u30fc\u3092\u8ffd\u52a0" } } }, diff --git a/homeassistant/components/intellifire/__init__.py b/homeassistant/components/intellifire/__init__.py index f5b6085781b..020136f078c 100644 --- a/homeassistant/components/intellifire/__init__.py +++ b/homeassistant/components/intellifire/__init__.py @@ -20,7 +20,13 @@ from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from .const import CONF_USER_ID, DOMAIN, LOGGER from .coordinator import IntellifireDataUpdateCoordinator -PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR, Platform.CLIMATE, Platform.SWITCH] +PLATFORMS = [ + Platform.BINARY_SENSOR, + Platform.CLIMATE, + Platform.FAN, + Platform.SENSOR, + Platform.SWITCH, +] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/intellifire/climate.py b/homeassistant/components/intellifire/climate.py index 1656be621e6..cf36e6b48a0 100644 --- a/homeassistant/components/intellifire/climate.py +++ b/homeassistant/components/intellifire/climate.py @@ -7,8 +7,8 @@ from homeassistant.components.climate import ( ClimateEntity, ClimateEntityDescription, ClimateEntityFeature, + HVACMode, ) -from homeassistant.components.climate.const import HVACMode from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS from homeassistant.core import HomeAssistant diff --git a/homeassistant/components/intellifire/fan.py b/homeassistant/components/intellifire/fan.py new file mode 100644 index 00000000000..d37bef2189a --- /dev/null +++ b/homeassistant/components/intellifire/fan.py @@ -0,0 +1,130 @@ +"""Fan definition for Intellifire.""" +from __future__ import annotations + +from collections.abc import Awaitable, Callable +from dataclasses import dataclass +import math +from typing import Any + +from intellifire4py import IntellifireControlAsync, IntellifirePollData + +from homeassistant.components.fan import ( + FanEntity, + FanEntityDescription, + FanEntityFeature, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.util.percentage import ( + percentage_to_ranged_value, + ranged_value_to_percentage, +) + +from .const import DOMAIN, LOGGER +from .coordinator import IntellifireDataUpdateCoordinator +from .entity import IntellifireEntity + + +@dataclass +class IntellifireFanRequiredKeysMixin: + """Required keys for fan entity.""" + + set_fn: Callable[[IntellifireControlAsync, int], Awaitable] + value_fn: Callable[[IntellifirePollData], bool] + speed_range: tuple[int, int] + + +@dataclass +class IntellifireFanEntityDescription( + FanEntityDescription, IntellifireFanRequiredKeysMixin +): + """Describes a fan entity.""" + + +INTELLIFIRE_FANS: tuple[IntellifireFanEntityDescription, ...] = ( + IntellifireFanEntityDescription( + key="fan", + name="Fan", + has_entity_name=True, + set_fn=lambda control_api, speed: control_api.set_fan_speed(speed=speed), + value_fn=lambda data: data.fanspeed, + speed_range=(1, 4), + ), +) + + +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_fan: + async_add_entities( + IntellifireFan(coordinator=coordinator, description=description) + for description in INTELLIFIRE_FANS + ) + return + LOGGER.debug("Disabling Fan - IntelliFire device does not appear to have one") + + +class IntellifireFan(IntellifireEntity, FanEntity): + """This is Fan entity for the fireplace.""" + + entity_description: IntellifireFanEntityDescription + _attr_supported_features = FanEntityFeature.SET_SPEED + + @property + def is_on(self) -> bool: + """Return on or off.""" + return self.entity_description.value_fn(self.coordinator.read_api.data) >= 1 + + @property + def percentage(self) -> int | None: + """Return fan percentage.""" + return ranged_value_to_percentage( + self.entity_description.speed_range, self.coordinator.read_api.data.fanspeed + ) + + @property + def speed_count(self) -> int: + """Count of supported speeds.""" + return self.entity_description.speed_range[1] + + async def async_set_percentage(self, percentage: int) -> None: + """Set the speed percentage of the fan.""" + # Calculate percentage steps + LOGGER.debug("Setting Fan Speed %s", percentage) + + int_value = math.ceil( + percentage_to_ranged_value(self.entity_description.speed_range, percentage) + ) + await self.entity_description.set_fn(self.coordinator.control_api, int_value) + await self.coordinator.async_request_refresh() + + async def async_turn_on( + self, + percentage: int | None = None, + preset_mode: str | None = None, + **kwargs: Any, + ) -> None: + """Turn on the fan.""" + if percentage: + int_value = math.ceil( + percentage_to_ranged_value( + self.entity_description.speed_range, percentage + ) + ) + else: + int_value = 1 + await self.entity_description.set_fn(self.coordinator.control_api, int_value) + await self.coordinator.async_request_refresh() + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn off the fan.""" + self.coordinator.control_api.fan_off() + await self.entity_description.set_fn(self.coordinator.control_api, 0) + await self.coordinator.async_request_refresh() diff --git a/homeassistant/components/intellifire/translations/cs.json b/homeassistant/components/intellifire/translations/cs.json index 8684c426280..9ac5e9d9099 100644 --- a/homeassistant/components/intellifire/translations/cs.json +++ b/homeassistant/components/intellifire/translations/cs.json @@ -1,16 +1,22 @@ { "config": { "abort": { - "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" }, "error": { "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" }, "flow_title": "{serial} ({host})", "step": { + "api_config": { + "data": { + "password": "Heslo" + } + }, "manual_device_entry": { "data": { - "host": "Hostitel" + "host": "Hostitel (IP adresa)" } }, "pick_device": { diff --git a/homeassistant/components/intellifire/translations/es.json b/homeassistant/components/intellifire/translations/es.json index 3cf2cfc6938..dcd4d7ed300 100644 --- a/homeassistant/components/intellifire/translations/es.json +++ b/homeassistant/components/intellifire/translations/es.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", "not_intellifire_device": "No es un dispositivo IntelliFire.", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "error": { "api_error": "Error de inicio de sesi\u00f3n", diff --git a/homeassistant/components/intellifire/translations/ja.json b/homeassistant/components/intellifire/translations/ja.json index 6c74e4743ed..0b09ffc61fe 100644 --- a/homeassistant/components/intellifire/translations/ja.json +++ b/homeassistant/components/intellifire/translations/ja.json @@ -23,7 +23,7 @@ }, "manual_device_entry": { "data": { - "host": "\u30db\u30b9\u30c8" + "host": "\u30db\u30b9\u30c8 (IP \u30a2\u30c9\u30ec\u30b9)" }, "description": "\u30ed\u30fc\u30ab\u30eb\u8a2d\u5b9a" }, diff --git a/homeassistant/components/intent/manifest.json b/homeassistant/components/intent/manifest.json index a0d5d0fca23..771482d76a4 100644 --- a/homeassistant/components/intent/manifest.json +++ b/homeassistant/components/intent/manifest.json @@ -4,5 +4,7 @@ "config_flow": false, "documentation": "https://www.home-assistant.io/integrations/intent", "dependencies": ["http"], - "codeowners": ["@home-assistant/core"] + "codeowners": ["@home-assistant/core"], + "quality_scale": "internal", + "integration_type": "system" } diff --git a/homeassistant/components/intesishome/climate.py b/homeassistant/components/intesishome/climate.py index b85c976a928..29cb30b81a2 100644 --- a/homeassistant/components/intesishome/climate.py +++ b/homeassistant/components/intesishome/climate.py @@ -8,9 +8,9 @@ from typing import Any, NamedTuple from pyintesishome import IHAuthenticationError, IHConnectionError, IntesisHome import voluptuous as vol -from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( ATTR_HVAC_MODE, + PLATFORM_SCHEMA, PRESET_BOOST, PRESET_COMFORT, PRESET_ECO, @@ -18,6 +18,7 @@ from homeassistant.components.climate.const import ( SWING_HORIZONTAL, SWING_OFF, SWING_VERTICAL, + ClimateEntity, ClimateEntityFeature, HVACMode, ) diff --git a/homeassistant/components/ipma/config_flow.py b/homeassistant/components/ipma/config_flow.py index 17fde104125..81ab8f98014 100644 --- a/homeassistant/components/ipma/config_flow.py +++ b/homeassistant/components/ipma/config_flow.py @@ -18,13 +18,6 @@ class IpmaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Init IpmaFlowHandler.""" self._errors = {} - async def async_step_import(self, config): - """Import a configuration from config.yaml.""" - - self._async_abort_entries_match(config) - config[CONF_MODE] = "daily" - return await self.async_step_user(user_input=config) - async def async_step_user(self, user_input=None): """Handle a flow initialized by the user.""" self._errors = {} diff --git a/homeassistant/components/iss/translations/ja.json b/homeassistant/components/iss/translations/ja.json index d53b9f8fecb..0568f747ed9 100644 --- a/homeassistant/components/iss/translations/ja.json +++ b/homeassistant/components/iss/translations/ja.json @@ -1,12 +1,12 @@ { "config": { "abort": { - "latitude_longitude_not_defined": "Home Assistant\u3067\u7def\u5ea6\u3068\u7d4c\u5ea6\u304c\u5b9a\u7fa9\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002", + "latitude_longitude_not_defined": "Home Assistant \u3067\u306f\u3001\u7def\u5ea6\u3068\u7d4c\u5ea6\u306f\u5b9a\u7fa9\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002", "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "step": { "user": { - "description": "\u56fd\u969b\u5b87\u5b99\u30b9\u30c6\u30fc\u30b7\u30e7\u30f3\u306e\u8a2d\u5b9a\u3092\u3057\u307e\u3059\u304b\uff1f" + "description": "\u56fd\u969b\u5b87\u5b99\u30b9\u30c6\u30fc\u30b7\u30e7\u30f3 (ISS) \u3092\u69cb\u6210\u3057\u307e\u3059\u304b?" } } }, diff --git a/homeassistant/components/isy994/__init__.py b/homeassistant/components/isy994/__init__.py index 301c86827e9..1b689e563dc 100644 --- a/homeassistant/components/isy994/__init__.py +++ b/homeassistant/components/isy994/__init__.py @@ -192,6 +192,10 @@ async def async_setup_entry( raise ConfigEntryNotReady( f"Invalid XML response from ISY; Ensure the ISY is running the latest firmware: {err}" ) from err + except TypeError as err: + raise ConfigEntryNotReady( + 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) diff --git a/homeassistant/components/isy994/climate.py b/homeassistant/components/isy994/climate.py index bc1f0353455..c141f856408 100644 --- a/homeassistant/components/isy994/climate.py +++ b/homeassistant/components/isy994/climate.py @@ -16,14 +16,14 @@ from pyisy.constants import ( ) from pyisy.nodes import Node -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, DOMAIN as CLIMATE, FAN_AUTO, FAN_OFF, FAN_ON, + ClimateEntity, ClimateEntityFeature, HVACAction, HVACMode, diff --git a/homeassistant/components/isy994/const.py b/homeassistant/components/isy994/const.py index ddabe1b9680..21cc23b01ca 100644 --- a/homeassistant/components/isy994/const.py +++ b/homeassistant/components/isy994/const.py @@ -2,7 +2,7 @@ import logging from homeassistant.components.binary_sensor import BinarySensorDeviceClass -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( FAN_AUTO, FAN_HIGH, FAN_MEDIUM, diff --git a/homeassistant/components/isy994/helpers.py b/homeassistant/components/isy994/helpers.py index 3b0de172a85..736cd12b9ea 100644 --- a/homeassistant/components/isy994/helpers.py +++ b/homeassistant/components/isy994/helpers.py @@ -17,7 +17,7 @@ from pyisy.programs import Programs from pyisy.variables import Variables from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR -from homeassistant.components.climate.const import DOMAIN as CLIMATE +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 diff --git a/homeassistant/components/isy994/translations/cs.json b/homeassistant/components/isy994/translations/cs.json index d165050bf77..45cd90895c7 100644 --- a/homeassistant/components/isy994/translations/cs.json +++ b/homeassistant/components/isy994/translations/cs.json @@ -13,7 +13,8 @@ "step": { "reauth_confirm": { "data": { - "password": "Heslo" + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" } }, "user": { diff --git a/homeassistant/components/isy994/translations/es.json b/homeassistant/components/isy994/translations/es.json index 0890cf8e251..b320167b3f1 100644 --- a/homeassistant/components/isy994/translations/es.json +++ b/homeassistant/components/isy994/translations/es.json @@ -7,7 +7,7 @@ "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "invalid_host": "La entrada del host no estaba en formato URL completo, por ejemplo, http://192.168.10.100:80", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente", "unknown": "Error inesperado" }, "flow_title": "{name} ({host})", diff --git a/homeassistant/components/isy994/translations/ja.json b/homeassistant/components/isy994/translations/ja.json index 60b69d07363..2ec57251d65 100644 --- a/homeassistant/components/isy994/translations/ja.json +++ b/homeassistant/components/isy994/translations/ja.json @@ -17,7 +17,7 @@ "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "username": "\u30e6\u30fc\u30b6\u30fc\u540d" }, - "description": "{host} \u306e\u8cc7\u683c\u60c5\u5831\u304c\u7121\u52b9\u306b\u306a\u308a\u307e\u3057\u305f\u3002", + "description": "{host}\u306e\u8a8d\u8a3c\u60c5\u5831\u304c\u7121\u52b9\u306b\u306a\u308a\u307e\u3057\u305f\u3002", "title": "ISY\u306e\u518d\u8a8d\u8a3c" }, "user": { diff --git a/homeassistant/components/itunes/media_player.py b/homeassistant/components/itunes/media_player.py index ea2cad37c77..c9b0e4a07af 100644 --- a/homeassistant/components/itunes/media_player.py +++ b/homeassistant/components/itunes/media_player.py @@ -10,22 +10,10 @@ from homeassistant.components.media_player import ( PLATFORM_SCHEMA, MediaPlayerEntity, MediaPlayerEntityFeature, + MediaPlayerState, + MediaType, ) -from homeassistant.components.media_player.const import ( - MEDIA_TYPE_MUSIC, - MEDIA_TYPE_PLAYLIST, -) -from homeassistant.const import ( - CONF_HOST, - CONF_NAME, - CONF_PORT, - CONF_SSL, - STATE_IDLE, - STATE_OFF, - STATE_ON, - STATE_PAUSED, - STATE_PLAYING, -) +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, CONF_SSL from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -195,6 +183,7 @@ def setup_platform( class ItunesDevice(MediaPlayerEntity): """Representation of an iTunes API instance.""" + _attr_media_content_type = MediaType.MUSIC _attr_supported_features = ( MediaPlayerEntityFeature.PAUSE | MediaPlayerEntityFeature.VOLUME_SET @@ -263,12 +252,12 @@ class ItunesDevice(MediaPlayerEntity): return "error" if self.player_state == "stopped": - return STATE_IDLE + return MediaPlayerState.IDLE if self.player_state == "paused": - return STATE_PAUSED + return MediaPlayerState.PAUSED - return STATE_PLAYING + return MediaPlayerState.PLAYING def update(self) -> None: """Retrieve latest state.""" @@ -312,16 +301,16 @@ class ItunesDevice(MediaPlayerEntity): """Content ID of current playing media.""" return self.content_id - @property - def media_content_type(self): - """Content type of current playing media.""" - return MEDIA_TYPE_MUSIC - @property def media_image_url(self): """Image url of current playing media.""" if ( - self.player_state in (STATE_PLAYING, STATE_IDLE, STATE_PAUSED) + self.player_state + in { + MediaPlayerState.PLAYING, + MediaPlayerState.IDLE, + MediaPlayerState.PAUSED, + } and self.current_title is not None ): return f"{self.client.artwork_url()}?id={self.content_id}" @@ -393,7 +382,7 @@ class ItunesDevice(MediaPlayerEntity): def play_media(self, media_type: str, media_id: str, **kwargs: Any) -> None: """Send the play_media command to the media player.""" - if media_type == MEDIA_TYPE_PLAYLIST: + if media_type == MediaType.PLAYLIST: response = self.client.play_playlist(media_id) self.update_state(response) @@ -406,6 +395,7 @@ class ItunesDevice(MediaPlayerEntity): class AirPlayDevice(MediaPlayerEntity): """Representation an AirPlay device via an iTunes API instance.""" + _attr_media_content_type = MediaType.MUSIC _attr_supported_features = ( MediaPlayerEntityFeature.VOLUME_SET | MediaPlayerEntityFeature.TURN_ON @@ -466,12 +456,12 @@ class AirPlayDevice(MediaPlayerEntity): return "mdi:volume-off" @property - def state(self): + def state(self) -> MediaPlayerState: """Return the state of the device.""" if self.selected is True: - return STATE_ON + return MediaPlayerState.ON - return STATE_OFF + return MediaPlayerState.OFF def update(self) -> None: """Retrieve latest state.""" @@ -481,11 +471,6 @@ class AirPlayDevice(MediaPlayerEntity): """Return the volume.""" return float(self.volume) / 100.0 - @property - def media_content_type(self): - """Flag of media content that is supported.""" - return MEDIA_TYPE_MUSIC - def set_volume_level(self, volume: float) -> None: """Set volume level, range 0..1.""" volume = int(volume * 100) diff --git a/homeassistant/components/izone/climate.py b/homeassistant/components/izone/climate.py index a80549f27fc..31ad5f7860d 100644 --- a/homeassistant/components/izone/climate.py +++ b/homeassistant/components/izone/climate.py @@ -7,8 +7,7 @@ from typing import Any from pizone import Controller, Zone import voluptuous as vol -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( FAN_AUTO, FAN_HIGH, FAN_LOW, @@ -16,6 +15,7 @@ from homeassistant.components.climate.const import ( FAN_TOP, PRESET_ECO, PRESET_NONE, + ClimateEntity, ClimateEntityFeature, HVACMode, ) @@ -295,7 +295,7 @@ class ControllerDevice(ClimateEntity): return key assert False, "Should be unreachable" - @property # type: ignore[misc] + @property @_return_on_connection_error([]) def hvac_modes(self) -> list[HVACMode]: """Return the list of available operation modes.""" @@ -303,13 +303,13 @@ class ControllerDevice(ClimateEntity): return [HVACMode.OFF, HVACMode.FAN_ONLY] return [HVACMode.OFF, *self._state_to_pizone] - @property # type: ignore[misc] + @property @_return_on_connection_error(PRESET_NONE) def preset_mode(self): """Eco mode is external air.""" return PRESET_ECO if self._controller.free_air else PRESET_NONE - @property # type: ignore[misc] + @property @_return_on_connection_error([PRESET_NONE]) def preset_modes(self): """Available preset modes, normal or eco.""" @@ -317,7 +317,7 @@ class ControllerDevice(ClimateEntity): return [PRESET_NONE, PRESET_ECO] return [PRESET_NONE] - @property # type: ignore[misc] + @property @_return_on_connection_error() def current_temperature(self) -> float | None: """Return the current temperature.""" @@ -347,7 +347,7 @@ class ControllerDevice(ClimateEntity): return None return zone.target_temperature - @property # type: ignore[misc] + @property @_return_on_connection_error() def target_temperature(self) -> float | None: """Return the temperature we try to reach (either from control zone or master unit).""" @@ -375,13 +375,13 @@ class ControllerDevice(ClimateEntity): """Return the list of available fan modes.""" return list(self._fan_to_pizone) - @property # type: ignore[misc] + @property @_return_on_connection_error(0.0) def min_temp(self) -> float: """Return the minimum temperature.""" return self._controller.temp_min - @property # type: ignore[misc] + @property @_return_on_connection_error(50.0) def max_temp(self) -> float: """Return the maximum temperature.""" @@ -516,7 +516,7 @@ class ZoneDevice(ClimateEntity): """Return the name of the entity.""" return self._name - @property # type: ignore[misc] + @property @_return_on_connection_error(0) def supported_features(self) -> int: """Return the list of supported features.""" diff --git a/homeassistant/components/jellyfin/media_source.py b/homeassistant/components/jellyfin/media_source.py index 662c0f0040a..2cb211acb9b 100644 --- a/homeassistant/components/jellyfin/media_source.py +++ b/homeassistant/components/jellyfin/media_source.py @@ -8,14 +8,7 @@ from typing import Any from jellyfin_apiclient_python.api import jellyfin_url from jellyfin_apiclient_python.client import JellyfinClient -from homeassistant.components.media_player.const import ( - MEDIA_CLASS_ALBUM, - MEDIA_CLASS_ARTIST, - MEDIA_CLASS_DIRECTORY, - MEDIA_CLASS_MOVIE, - MEDIA_CLASS_TRACK, -) -from homeassistant.components.media_player.errors import BrowseError +from homeassistant.components.media_player import BrowseError, MediaClass from homeassistant.components.media_source.models import ( BrowseMediaSource, MediaSource, @@ -113,12 +106,12 @@ class JellyfinSource(MediaSource): base = BrowseMediaSource( domain=DOMAIN, identifier=None, - media_class=MEDIA_CLASS_DIRECTORY, + media_class=MediaClass.DIRECTORY, media_content_type=MEDIA_TYPE_NONE, title=self.name, can_play=False, can_expand=True, - children_media_class=MEDIA_CLASS_DIRECTORY, + children_media_class=MediaClass.DIRECTORY, ) libraries = await self._get_libraries() @@ -164,7 +157,7 @@ class JellyfinSource(MediaSource): result = BrowseMediaSource( domain=DOMAIN, identifier=library_id, - media_class=MEDIA_CLASS_DIRECTORY, + media_class=MediaClass.DIRECTORY, media_content_type=MEDIA_TYPE_NONE, title=library_name, can_play=False, @@ -172,10 +165,10 @@ class JellyfinSource(MediaSource): ) if include_children: - result.children_media_class = MEDIA_CLASS_ARTIST + result.children_media_class = MediaClass.ARTIST result.children = await self._build_artists(library_id) if not result.children: - result.children_media_class = MEDIA_CLASS_ALBUM + result.children_media_class = MediaClass.ALBUM result.children = await self._build_albums(library_id) return result @@ -197,7 +190,7 @@ class JellyfinSource(MediaSource): result = BrowseMediaSource( domain=DOMAIN, identifier=artist_id, - media_class=MEDIA_CLASS_ARTIST, + media_class=MediaClass.ARTIST, media_content_type=MEDIA_TYPE_NONE, title=artist_name, can_play=False, @@ -206,7 +199,7 @@ class JellyfinSource(MediaSource): ) if include_children: - result.children_media_class = MEDIA_CLASS_ALBUM + result.children_media_class = MediaClass.ALBUM result.children = await self._build_albums(artist_id) return result @@ -228,7 +221,7 @@ class JellyfinSource(MediaSource): result = BrowseMediaSource( domain=DOMAIN, identifier=album_id, - media_class=MEDIA_CLASS_ALBUM, + media_class=MediaClass.ALBUM, media_content_type=MEDIA_TYPE_NONE, title=album_title, can_play=False, @@ -237,7 +230,7 @@ class JellyfinSource(MediaSource): ) if include_children: - result.children_media_class = MEDIA_CLASS_TRACK + result.children_media_class = MediaClass.TRACK result.children = await self._build_tracks(album_id) return result @@ -264,7 +257,7 @@ class JellyfinSource(MediaSource): result = BrowseMediaSource( domain=DOMAIN, identifier=track_id, - media_class=MEDIA_CLASS_TRACK, + media_class=MediaClass.TRACK, media_content_type=mime_type, title=track_title, can_play=True, @@ -284,7 +277,7 @@ class JellyfinSource(MediaSource): result = BrowseMediaSource( domain=DOMAIN, identifier=library_id, - media_class=MEDIA_CLASS_DIRECTORY, + media_class=MediaClass.DIRECTORY, media_content_type=MEDIA_TYPE_NONE, title=library_name, can_play=False, @@ -292,7 +285,7 @@ class JellyfinSource(MediaSource): ) if include_children: - result.children_media_class = MEDIA_CLASS_MOVIE + result.children_media_class = MediaClass.MOVIE result.children = await self._build_movies(library_id) return result @@ -313,7 +306,7 @@ class JellyfinSource(MediaSource): result = BrowseMediaSource( domain=DOMAIN, identifier=movie_id, - media_class=MEDIA_CLASS_MOVIE, + media_class=MediaClass.MOVIE, media_content_type=mime_type, title=movie_title, can_play=True, diff --git a/homeassistant/components/jellyfin/translations/cs.json b/homeassistant/components/jellyfin/translations/cs.json index 5d03904568e..c0841233cb7 100644 --- a/homeassistant/components/jellyfin/translations/cs.json +++ b/homeassistant/components/jellyfin/translations/cs.json @@ -4,6 +4,14 @@ "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/juicenet/number.py b/homeassistant/components/juicenet/number.py index c7f444b83e2..45be1dd9004 100644 --- a/homeassistant/components/juicenet/number.py +++ b/homeassistant/components/juicenet/number.py @@ -5,8 +5,11 @@ from dataclasses import dataclass from pyjuicenet import Api, Charger -from homeassistant.components.number import NumberEntity, NumberEntityDescription -from homeassistant.components.number.const import DEFAULT_MAX_VALUE +from homeassistant.components.number import ( + DEFAULT_MAX_VALUE, + NumberEntity, + NumberEntityDescription, +) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback diff --git a/homeassistant/components/juicenet/switch.py b/homeassistant/components/juicenet/switch.py index c7e2f499e04..576c66c0841 100644 --- a/homeassistant/components/juicenet/switch.py +++ b/homeassistant/components/juicenet/switch.py @@ -1,4 +1,6 @@ """Support for monitoring juicenet/juicepoint/juicebox based EVSE switches.""" +from typing import Any + from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -41,10 +43,10 @@ class JuiceNetChargeNowSwitch(JuiceNetDevice, SwitchEntity): """Return true if switch is on.""" return self.device.override_time != 0 - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Charge now.""" await self.device.set_override(True) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Don't charge now.""" await self.device.set_override(False) diff --git a/homeassistant/components/justnimbus/sensor.py b/homeassistant/components/justnimbus/sensor.py index 73a68ac9139..41f1e81d5b3 100644 --- a/homeassistant/components/justnimbus/sensor.py +++ b/homeassistant/components/justnimbus/sensor.py @@ -103,6 +103,7 @@ SENSOR_TYPES = ( name="Reservoir content", icon="mdi:car-coolant-level", native_unit_of_measurement=VOLUME_LITERS, + device_class=SensorDeviceClass.VOLUME, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda coordinator: coordinator.data.reservoir_content, @@ -112,6 +113,7 @@ SENSOR_TYPES = ( name="Total saved", icon="mdi:water-opacity", native_unit_of_measurement=VOLUME_LITERS, + device_class=SensorDeviceClass.VOLUME, state_class=SensorStateClass.TOTAL_INCREASING, entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda coordinator: coordinator.data.total_saved, @@ -121,6 +123,7 @@ SENSOR_TYPES = ( name="Total replenished", icon="mdi:water", native_unit_of_measurement=VOLUME_LITERS, + device_class=SensorDeviceClass.VOLUME, state_class=SensorStateClass.TOTAL_INCREASING, entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda coordinator: coordinator.data.total_replenished, @@ -138,6 +141,7 @@ SENSOR_TYPES = ( name="Total use", icon="mdi:chart-donut", native_unit_of_measurement=VOLUME_LITERS, + device_class=SensorDeviceClass.VOLUME, state_class=SensorStateClass.TOTAL_INCREASING, entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda coordinator: coordinator.data.totver, @@ -147,6 +151,7 @@ SENSOR_TYPES = ( name="Max reservoir content", icon="mdi:waves", native_unit_of_measurement=VOLUME_LITERS, + device_class=SensorDeviceClass.VOLUME, state_class=SensorStateClass.TOTAL, entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda coordinator: coordinator.data.reservoir_content_max, diff --git a/homeassistant/components/justnimbus/translations/bg.json b/homeassistant/components/justnimbus/translations/bg.json new file mode 100644 index 00000000000..809ac784bde --- /dev/null +++ b/homeassistant/components/justnimbus/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_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "user": { + "data": { + "client_id": "ID \u043d\u0430 \u043a\u043b\u0438\u0435\u043d\u0442\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/justnimbus/translations/cs.json b/homeassistant/components/justnimbus/translations/cs.json new file mode 100644 index 00000000000..19a31a3f9cb --- /dev/null +++ b/homeassistant/components/justnimbus/translations/cs.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/justnimbus/translations/nl.json b/homeassistant/components/justnimbus/translations/nl.json index 70d636c953e..87e2082c011 100644 --- a/homeassistant/components/justnimbus/translations/nl.json +++ b/homeassistant/components/justnimbus/translations/nl.json @@ -4,7 +4,9 @@ "already_configured": "Apparaat is al geconfigureerd" }, "error": { - "cannot_connect": "Kan geen verbinding maken" + "cannot_connect": "Kan geen verbinding maken", + "invalid_auth": "Ongeldige authenticatie", + "unknown": "Onverwachte fout" } } } \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/sk.json b/homeassistant/components/justnimbus/translations/pt.json similarity index 50% rename from homeassistant/components/flunearyou/translations/sk.json rename to homeassistant/components/justnimbus/translations/pt.json index e6945904d90..8df0a5b68e3 100644 --- a/homeassistant/components/flunearyou/translations/sk.json +++ b/homeassistant/components/justnimbus/translations/pt.json @@ -3,8 +3,7 @@ "step": { "user": { "data": { - "latitude": "Zemepisn\u00e1 \u0161\u00edrka", - "longitude": "Zemepisn\u00e1 d\u013a\u017eka" + "client_id": "ID do Cliente" } } } diff --git a/homeassistant/components/justnimbus/translations/sv.json b/homeassistant/components/justnimbus/translations/sv.json new file mode 100644 index 00000000000..a255fd3d8fb --- /dev/null +++ b/homeassistant/components/justnimbus/translations/sv.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "data": { + "client_id": "Klient ID" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kaiterra/sensor.py b/homeassistant/components/kaiterra/sensor.py index 664c29110a1..29013052c1c 100644 --- a/homeassistant/components/kaiterra/sensor.py +++ b/homeassistant/components/kaiterra/sensor.py @@ -87,7 +87,7 @@ class KaiterraSensor(SensorEntity): ) @property - def available(self): + def available(self) -> bool: """Return the availability of the sensor.""" return self._api.data.get(self._device_id) is not None @@ -110,7 +110,7 @@ class KaiterraSensor(SensorEntity): return TEMP_CELSIUS return value - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register callback.""" self.async_on_remove( async_dispatcher_connect( diff --git a/homeassistant/components/kaleidescape/media_player.py b/homeassistant/components/kaleidescape/media_player.py index da70643f8ee..cbae7f0df76 100644 --- a/homeassistant/components/kaleidescape/media_player.py +++ b/homeassistant/components/kaleidescape/media_player.py @@ -9,8 +9,8 @@ from kaleidescape import const as kaleidescape_const from homeassistant.components.media_player import ( MediaPlayerEntity, MediaPlayerEntityFeature, + MediaPlayerState, ) -from homeassistant.const import STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING from homeassistant.util.dt import utcnow from .const import DOMAIN as KALEIDESCAPE_DOMAIN @@ -86,15 +86,15 @@ class KaleidescapeMediaPlayer(KaleidescapeEntity, MediaPlayerEntity): await self._device.previous() @property - def state(self) -> str: + def state(self) -> MediaPlayerState: """State of device.""" if self._device.power.state == kaleidescape_const.DEVICE_POWER_STATE_STANDBY: - return STATE_OFF + return MediaPlayerState.OFF if self._device.movie.play_status in KALEIDESCAPE_PLAYING_STATES: - return STATE_PLAYING + return MediaPlayerState.PLAYING if self._device.movie.play_status in KALEIDESCAPE_PAUSED_STATES: - return STATE_PAUSED - return STATE_IDLE + return MediaPlayerState.PAUSED + return MediaPlayerState.IDLE @property def available(self) -> bool: diff --git a/homeassistant/components/kaleidescape/translations/cs.json b/homeassistant/components/kaleidescape/translations/cs.json index deb0693b00d..9044ce69fb1 100644 --- a/homeassistant/components/kaleidescape/translations/cs.json +++ b/homeassistant/components/kaleidescape/translations/cs.json @@ -8,6 +8,13 @@ "error": { "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" }, - "flow_title": "{model} ({name})" + "flow_title": "{model} ({name})", + "step": { + "user": { + "data": { + "host": "Hostitel" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/kankun/switch.py b/homeassistant/components/kankun/switch.py index 69a3dcee301..f64b11706a1 100644 --- a/homeassistant/components/kankun/switch.py +++ b/homeassistant/components/kankun/switch.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any import requests import voluptuous as vol @@ -114,16 +115,16 @@ class KankunSwitch(SwitchEntity): """Return true if device is on.""" return self._state - def update(self): + def update(self) -> None: """Update device state.""" self._state = self._query_state() - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" if self._switch("on"): self._state = True - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" if self._switch("off"): self._state = False diff --git a/homeassistant/components/keenetic_ndms2/binary_sensor.py b/homeassistant/components/keenetic_ndms2/binary_sensor.py index 5b8fa952e18..fa9b1fd48dd 100644 --- a/homeassistant/components/keenetic_ndms2/binary_sensor.py +++ b/homeassistant/components/keenetic_ndms2/binary_sensor.py @@ -53,7 +53,7 @@ class RouterOnlineBinarySensor(BinarySensorEntity): """Return a client description for device registry.""" return self._router.device_info - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Client entity created.""" self.async_on_remove( async_dispatcher_connect( diff --git a/homeassistant/components/keenetic_ndms2/const.py b/homeassistant/components/keenetic_ndms2/const.py index c07fb0a0d15..0b415a9502f 100644 --- a/homeassistant/components/keenetic_ndms2/const.py +++ b/homeassistant/components/keenetic_ndms2/const.py @@ -1,6 +1,6 @@ """Constants used in the Keenetic NDMS2 components.""" -from homeassistant.components.device_tracker.const import ( +from homeassistant.components.device_tracker import ( DEFAULT_CONSIDER_HOME as _DEFAULT_CONSIDER_HOME, ) diff --git a/homeassistant/components/keenetic_ndms2/device_tracker.py b/homeassistant/components/keenetic_ndms2/device_tracker.py index 116f82afe3a..a2b01040a5a 100644 --- a/homeassistant/components/keenetic_ndms2/device_tracker.py +++ b/homeassistant/components/keenetic_ndms2/device_tracker.py @@ -97,10 +97,10 @@ class KeeneticTracker(ScannerEntity): ) @property - def is_connected(self): + def is_connected(self) -> bool: """Return true if the device is connected to the network.""" return ( - self._last_seen + self._last_seen is not None and (dt_util.utcnow() - self._last_seen) < self._router.consider_home_interval ) @@ -144,12 +144,12 @@ class KeeneticTracker(ScannerEntity): } return None - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Client entity created.""" _LOGGER.debug("New network device tracker %s (%s)", self.name, self.unique_id) @callback - def update_device(): + def update_device() -> None: _LOGGER.debug( "Updating Keenetic tracked device %s (%s)", self.entity_id, diff --git a/homeassistant/components/kef/media_player.py b/homeassistant/components/kef/media_player.py index 9ed669c3201..078354c705e 100644 --- a/homeassistant/components/kef/media_player.py +++ b/homeassistant/components/kef/media_player.py @@ -15,15 +15,9 @@ from homeassistant.components.media_player import ( PLATFORM_SCHEMA, MediaPlayerEntity, MediaPlayerEntityFeature, + MediaPlayerState, ) -from homeassistant.const import ( - CONF_HOST, - CONF_NAME, - CONF_PORT, - CONF_TYPE, - STATE_OFF, - STATE_ON, -) +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, CONF_TYPE from homeassistant.core import HomeAssistant from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers import config_validation as cv, entity_platform @@ -190,6 +184,8 @@ async def async_setup_platform( class KefMediaPlayer(MediaPlayerEntity): """Kef Player Object.""" + _attr_icon = "mdi:speaker-wireless" + def __init__( self, name, @@ -206,8 +202,8 @@ class KefMediaPlayer(MediaPlayerEntity): unique_id, ): """Initialize the media player.""" - self._name = name - self._sources = sources + self._attr_name = name + self._attr_source_list = sources self._speaker = AsyncKefSpeaker( host, port, @@ -217,15 +213,11 @@ class KefMediaPlayer(MediaPlayerEntity): inverse_speaker_mode, loop=loop, ) - self._unique_id = unique_id + self._attr_unique_id = unique_id self._supports_on = supports_on self._speaker_type = speaker_type - self._state = None - self._muted = None - self._source = None - self._volume = None - self._is_online = None + self._attr_available = False self._dsp = None self._update_dsp_task_remover = None @@ -243,131 +235,88 @@ class KefMediaPlayer(MediaPlayerEntity): if supports_on: self._attr_supported_features |= MediaPlayerEntityFeature.TURN_ON - @property - def name(self): - """Return the name of the device.""" - return self._name - - @property - def state(self): - """Return the state of the device.""" - return self._state - - async def async_update(self): + async def async_update(self) -> None: """Update latest state.""" _LOGGER.debug("Running async_update") try: - self._is_online = await self._speaker.is_online() - if self._is_online: + self._attr_available = await self._speaker.is_online() + if self.available: ( - self._volume, - self._muted, + self._attr_volume_level, + self._attr_is_volume_muted, ) = await self._speaker.get_volume_and_is_muted() state = await self._speaker.get_state() - self._source = state.source - self._state = STATE_ON if state.is_on else STATE_OFF + self._attr_source = state.source + self._attr_state = ( + MediaPlayerState.ON if state.is_on else MediaPlayerState.OFF + ) if self._dsp is None: # Only do this when necessary because it is a slow operation await self.update_dsp() else: - self._muted = None - self._source = None - self._volume = None - self._state = STATE_OFF + self._attr_is_volume_muted = None + self._attr_source = None + self._attr_volume_level = None + self._attr_state = MediaPlayerState.OFF except (ConnectionError, TimeoutError) as err: _LOGGER.debug("Error in `update`: %s", err) - self._state = None + self._attr_state = None - @property - def volume_level(self): - """Volume level of the media player (0..1).""" - return self._volume - - @property - def is_volume_muted(self): - """Boolean if volume is currently muted.""" - return self._muted - - @property - def source(self): - """Name of the current input source.""" - return self._source - - @property - def source_list(self): - """List of available input sources.""" - return self._sources - - @property - def available(self): - """Return if the speaker is reachable online.""" - return self._is_online - - @property - def unique_id(self): - """Return the device unique id.""" - return self._unique_id - - @property - def icon(self): - """Return the device's icon.""" - return "mdi:speaker-wireless" - - async def async_turn_off(self): + async def async_turn_off(self) -> None: """Turn the media player off.""" await self._speaker.turn_off() - async def async_turn_on(self): + async def async_turn_on(self) -> None: """Turn the media player on.""" if not self._supports_on: raise NotImplementedError() await self._speaker.turn_on() - async def async_volume_up(self): + async def async_volume_up(self) -> None: """Volume up the media player.""" await self._speaker.increase_volume() - async def async_volume_down(self): + async def async_volume_down(self) -> None: """Volume down the media player.""" await self._speaker.decrease_volume() - async def async_set_volume_level(self, volume): + async def async_set_volume_level(self, volume: float) -> None: """Set volume level, range 0..1.""" await self._speaker.set_volume(volume) - async def async_mute_volume(self, mute): + async def async_mute_volume(self, mute: bool) -> None: """Mute (True) or unmute (False) media player.""" if mute: await self._speaker.mute() else: await self._speaker.unmute() - async def async_select_source(self, source: str): + async def async_select_source(self, source: str) -> None: """Select input source.""" - if source in self.source_list: + if self.source_list is not None and source in self.source_list: await self._speaker.set_source(source) else: raise ValueError(f"Unknown input source: {source}.") - async def async_media_play(self): + async def async_media_play(self) -> None: """Send play command.""" await self._speaker.set_play_pause() - async def async_media_pause(self): + async def async_media_pause(self) -> None: """Send pause command.""" await self._speaker.set_play_pause() - async def async_media_previous_track(self): + async def async_media_previous_track(self) -> None: """Send previous track command.""" await self._speaker.prev_track() - async def async_media_next_track(self): + async def async_media_next_track(self) -> None: """Send next track command.""" await self._speaker.next_track() async def update_dsp(self, _=None) -> None: """Update the DSP settings.""" - if self._speaker_type == "LS50" and self._state == STATE_OFF: + if self._speaker_type == "LS50" and self.state == MediaPlayerState.OFF: # The LSX is able to respond when off the LS50 has to be on. return @@ -382,13 +331,13 @@ class KefMediaPlayer(MediaPlayerEntity): **mode._asdict(), ) - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Subscribe to DSP updates.""" self._update_dsp_task_remover = async_track_time_interval( self.hass, self.update_dsp, DSP_SCAN_INTERVAL ) - async def async_will_remove_from_hass(self): + async def async_will_remove_from_hass(self) -> None: """Unsubscribe to DSP updates.""" self._update_dsp_task_remover() self._update_dsp_task_remover = None diff --git a/homeassistant/components/kegtron/__init__.py b/homeassistant/components/kegtron/__init__.py new file mode 100644 index 00000000000..7a1669bdcd4 --- /dev/null +++ b/homeassistant/components/kegtron/__init__.py @@ -0,0 +1,49 @@ +"""The Kegtron integration.""" +from __future__ import annotations + +import logging + +from kegtron_ble import KegtronBluetoothDeviceData + +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 Kegtron BLE device from a config entry.""" + address = entry.unique_id + assert address is not None + data = KegtronBluetoothDeviceData() + 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/kegtron/config_flow.py b/homeassistant/components/kegtron/config_flow.py new file mode 100644 index 00000000000..cc0457af87b --- /dev/null +++ b/homeassistant/components/kegtron/config_flow.py @@ -0,0 +1,94 @@ +"""Config flow for Kegtron ble integration.""" +from __future__ import annotations + +from typing import Any + +from kegtron_ble import KegtronBluetoothDeviceData 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 KegtronConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for kegtron.""" + + 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/kegtron/const.py b/homeassistant/components/kegtron/const.py new file mode 100644 index 00000000000..884ea8f635c --- /dev/null +++ b/homeassistant/components/kegtron/const.py @@ -0,0 +1,3 @@ +"""Constants for the Kegtron integration.""" + +DOMAIN = "kegtron" diff --git a/homeassistant/components/kegtron/device.py b/homeassistant/components/kegtron/device.py new file mode 100644 index 00000000000..b97aed76b7d --- /dev/null +++ b/homeassistant/components/kegtron/device.py @@ -0,0 +1,35 @@ +"""Support for Kegtron devices.""" +from __future__ import annotations + +import logging + +from kegtron_ble import DeviceKey, SensorDeviceInfo + +from homeassistant.components.bluetooth.passive_update_processor import ( + PassiveBluetoothEntityKey, +) +from homeassistant.const import ATTR_MANUFACTURER, ATTR_MODEL, ATTR_NAME +from homeassistant.helpers.entity import DeviceInfo + +_LOGGER = logging.getLogger(__name__) + + +def device_key_to_bluetooth_entity_key( + device_key: DeviceKey, +) -> PassiveBluetoothEntityKey: + """Convert a device key to an entity key.""" + return PassiveBluetoothEntityKey(device_key.key, device_key.device_id) + + +def sensor_device_info_to_hass( + sensor_device_info: SensorDeviceInfo, +) -> DeviceInfo: + """Convert a sensor device info to a sensor device info.""" + hass_device_info = DeviceInfo({}) + if sensor_device_info.name is not None: + hass_device_info[ATTR_NAME] = sensor_device_info.name + if sensor_device_info.manufacturer is not None: + hass_device_info[ATTR_MANUFACTURER] = sensor_device_info.manufacturer + if sensor_device_info.model is not None: + hass_device_info[ATTR_MODEL] = sensor_device_info.model + return hass_device_info diff --git a/homeassistant/components/kegtron/manifest.json b/homeassistant/components/kegtron/manifest.json new file mode 100644 index 00000000000..c3205736226 --- /dev/null +++ b/homeassistant/components/kegtron/manifest.json @@ -0,0 +1,16 @@ +{ + "domain": "kegtron", + "name": "Kegtron", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/kegtron", + "bluetooth": [ + { + "connectable": false, + "manufacturer_id": 65535 + } + ], + "requirements": ["kegtron-ble==0.4.0"], + "dependencies": ["bluetooth"], + "codeowners": ["@Ernst79"], + "iot_class": "local_push" +} diff --git a/homeassistant/components/kegtron/sensor.py b/homeassistant/components/kegtron/sensor.py new file mode 100644 index 00000000000..892d8651185 --- /dev/null +++ b/homeassistant/components/kegtron/sensor.py @@ -0,0 +1,138 @@ +"""Support for Kegtron sensors.""" +from __future__ import annotations + +from typing import Optional, Union + +from kegtron_ble import ( + SensorDeviceClass as KegtronSensorDeviceClass, + SensorUpdate, + Units, +) + +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 SIGNAL_STRENGTH_DECIBELS_MILLIWATT, VOLUME_LITERS +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .device import device_key_to_bluetooth_entity_key, sensor_device_info_to_hass + +SENSOR_DESCRIPTIONS = { + KegtronSensorDeviceClass.PORT_COUNT: SensorEntityDescription( + key=KegtronSensorDeviceClass.PORT_COUNT, + icon="mdi:water-pump", + ), + KegtronSensorDeviceClass.KEG_SIZE: SensorEntityDescription( + key=KegtronSensorDeviceClass.KEG_SIZE, + icon="mdi:keg", + native_unit_of_measurement=VOLUME_LITERS, + device_class=SensorDeviceClass.VOLUME, + state_class=SensorStateClass.MEASUREMENT, + ), + KegtronSensorDeviceClass.KEG_TYPE: SensorEntityDescription( + key=KegtronSensorDeviceClass.KEG_TYPE, + icon="mdi:keg", + ), + KegtronSensorDeviceClass.VOLUME_START: SensorEntityDescription( + key=KegtronSensorDeviceClass.VOLUME_START, + icon="mdi:keg", + native_unit_of_measurement=VOLUME_LITERS, + device_class=SensorDeviceClass.VOLUME, + state_class=SensorStateClass.MEASUREMENT, + ), + KegtronSensorDeviceClass.VOLUME_DISPENSED: SensorEntityDescription( + key=KegtronSensorDeviceClass.VOLUME_DISPENSED, + icon="mdi:keg", + native_unit_of_measurement=VOLUME_LITERS, + device_class=SensorDeviceClass.VOLUME, + state_class=SensorStateClass.TOTAL, + ), + KegtronSensorDeviceClass.PORT_STATE: SensorEntityDescription( + key=KegtronSensorDeviceClass.PORT_STATE, + icon="mdi:water-pump", + ), + KegtronSensorDeviceClass.PORT_NAME: SensorEntityDescription( + key=KegtronSensorDeviceClass.PORT_NAME, + icon="mdi:water-pump", + ), + KegtronSensorDeviceClass.SIGNAL_STRENGTH: SensorEntityDescription( + key=f"{KegtronSensorDeviceClass.SIGNAL_STRENGTH}_{Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT}", + 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, + ), +} + + +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) + for device_id, device_info in sensor_update.devices.items() + }, + entity_descriptions={ + device_key_to_bluetooth_entity_key(device_key): SENSOR_DESCRIPTIONS[ + description.device_class + ] + for device_key, description in sensor_update.entity_descriptions.items() + if description.device_class + }, + 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 Kegtron 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( + KegtronBluetoothSensorEntity, async_add_entities + ) + ) + entry.async_on_unload(coordinator.async_register_processor(processor)) + + +class KegtronBluetoothSensorEntity( + PassiveBluetoothProcessorEntity[ + PassiveBluetoothDataProcessor[Optional[Union[float, int]]] + ], + SensorEntity, +): + """Representation of a Kegtron 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/kegtron/strings.json b/homeassistant/components/kegtron/strings.json new file mode 100644 index 00000000000..a045d84771e --- /dev/null +++ b/homeassistant/components/kegtron/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/kegtron/translations/bg.json b/homeassistant/components/kegtron/translations/bg.json new file mode 100644 index 00000000000..af9a13197df --- /dev/null +++ b/homeassistant/components/kegtron/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/kegtron/translations/ca.json b/homeassistant/components/kegtron/translations/ca.json new file mode 100644 index 00000000000..c121ff7408c --- /dev/null +++ b/homeassistant/components/kegtron/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/kegtron/translations/de.json b/homeassistant/components/kegtron/translations/de.json new file mode 100644 index 00000000000..4c5720ec6fb --- /dev/null +++ b/homeassistant/components/kegtron/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/kegtron/translations/el.json b/homeassistant/components/kegtron/translations/el.json new file mode 100644 index 00000000000..cdb57c8ac1b --- /dev/null +++ b/homeassistant/components/kegtron/translations/el.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", + "already_in_progress": "\u0397 \u03c1\u03bf\u03ae \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2 \u03b2\u03c1\u03af\u03c3\u03ba\u03b5\u03c4\u03b1\u03b9 \u03ae\u03b4\u03b7 \u03c3\u03b5 \u03b5\u03be\u03ad\u03bb\u03b9\u03be\u03b7", + "no_devices_found": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03c3\u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03bf", + "not_supported": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {name};" + }, + "user": { + "data": { + "address": "\u03a3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae" + }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b3\u03b9\u03b1 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kegtron/translations/en.json b/homeassistant/components/kegtron/translations/en.json new file mode 100644 index 00000000000..ebd9760c161 --- /dev/null +++ b/homeassistant/components/kegtron/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 setup {name}?" + }, + "user": { + "data": { + "address": "Device" + }, + "description": "Choose a device to setup" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kegtron/translations/es.json b/homeassistant/components/kegtron/translations/es.json new file mode 100644 index 00000000000..ae0ab01acdf --- /dev/null +++ b/homeassistant/components/kegtron/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/kegtron/translations/fr.json b/homeassistant/components/kegtron/translations/fr.json new file mode 100644 index 00000000000..8ddb4af4dbc --- /dev/null +++ b/homeassistant/components/kegtron/translations/fr.json @@ -0,0 +1,22 @@ +{ + "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", + "not_supported": "Appareil non pris en charge" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Voulez-vous configurer {name}\u00a0?" + }, + "user": { + "data": { + "address": "Appareil" + }, + "description": "S\u00e9lectionnez l'appareil \u00e0 configurer" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kegtron/translations/he.json b/homeassistant/components/kegtron/translations/he.json new file mode 100644 index 00000000000..de780eb221a --- /dev/null +++ b/homeassistant/components/kegtron/translations/he.json @@ -0,0 +1,21 @@ +{ + "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 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea {name}?" + }, + "user": { + "data": { + "address": "\u05d4\u05ea\u05e7\u05df" + }, + "description": "\u05d1\u05d7\u05d9\u05e8\u05ea \u05d4\u05ea\u05e7\u05df \u05dc\u05d4\u05d2\u05d3\u05e8\u05d4" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kegtron/translations/hu.json b/homeassistant/components/kegtron/translations/hu.json new file mode 100644 index 00000000000..97fbb5b9408 --- /dev/null +++ b/homeassistant/components/kegtron/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\u00e1lassza ki a be\u00e1ll\u00edtani k\u00edv\u00e1nt eszk\u00f6zt" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kegtron/translations/id.json b/homeassistant/components/kegtron/translations/id.json new file mode 100644 index 00000000000..573eb39ed15 --- /dev/null +++ b/homeassistant/components/kegtron/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/kegtron/translations/it.json b/homeassistant/components/kegtron/translations/it.json new file mode 100644 index 00000000000..7784ed3a240 --- /dev/null +++ b/homeassistant/components/kegtron/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": "Seleziona un dispositivo da configurare" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kegtron/translations/nl.json b/homeassistant/components/kegtron/translations/nl.json new file mode 100644 index 00000000000..320c86529fe --- /dev/null +++ b/homeassistant/components/kegtron/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 is 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/kegtron/translations/no.json b/homeassistant/components/kegtron/translations/no.json new file mode 100644 index 00000000000..0bf8b1695ec --- /dev/null +++ b/homeassistant/components/kegtron/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 konfigurere {name}?" + }, + "user": { + "data": { + "address": "Enhet" + }, + "description": "Velg en enhet du vil konfigurere" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kegtron/translations/pl.json b/homeassistant/components/kegtron/translations/pl.json new file mode 100644 index 00000000000..4715905a2e9 --- /dev/null +++ b/homeassistant/components/kegtron/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/kegtron/translations/pt-BR.json b/homeassistant/components/kegtron/translations/pt-BR.json new file mode 100644 index 00000000000..0da7639fa2a --- /dev/null +++ b/homeassistant/components/kegtron/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/kegtron/translations/ru.json b/homeassistant/components/kegtron/translations/ru.json new file mode 100644 index 00000000000..887499e5f2e --- /dev/null +++ b/homeassistant/components/kegtron/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/kegtron/translations/zh-Hant.json b/homeassistant/components/kegtron/translations/zh-Hant.json new file mode 100644 index 00000000000..64ae1f19094 --- /dev/null +++ b/homeassistant/components/kegtron/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/keymitt_ble/__init__.py b/homeassistant/components/keymitt_ble/__init__.py new file mode 100644 index 00000000000..1a7df4fe0a9 --- /dev/null +++ b/homeassistant/components/keymitt_ble/__init__.py @@ -0,0 +1,50 @@ +"""Integration to integrate Keymitt BLE devices with Home Assistant.""" +from __future__ import annotations + +import logging + +from microbot import MicroBotApiClient + +from homeassistant.components import bluetooth +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_ADDRESS, Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady + +from .const import DOMAIN +from .coordinator import MicroBotDataUpdateCoordinator + +_LOGGER: logging.Logger = logging.getLogger(__package__) +PLATFORMS: list[str] = [Platform.SWITCH] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up this integration using UI.""" + hass.data.setdefault(DOMAIN, {}) + token: str = entry.data[CONF_ACCESS_TOKEN] + bdaddr: str = entry.data[CONF_ADDRESS] + ble_device = bluetooth.async_ble_device_from_address(hass, bdaddr) + if not ble_device: + raise ConfigEntryNotReady(f"Could not find MicroBot with address {bdaddr}") + client = MicroBotApiClient( + device=ble_device, + token=token, + ) + coordinator = MicroBotDataUpdateCoordinator( + hass, client=client, ble_device=ble_device + ) + + hass.data[DOMAIN][entry.entry_id] = coordinator + + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + entry.async_on_unload(coordinator.async_start()) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Handle removal of an entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/keymitt_ble/config_flow.py b/homeassistant/components/keymitt_ble/config_flow.py new file mode 100644 index 00000000000..8a8a954abd6 --- /dev/null +++ b/homeassistant/components/keymitt_ble/config_flow.py @@ -0,0 +1,157 @@ +"""Adds config flow for MicroBot.""" +from __future__ import annotations + +import logging +from typing import Any + +from bleak.backends.device import BLEDevice +from microbot import ( + MicroBotAdvertisement, + MicroBotApiClient, + parse_advertisement_data, + randomid, +) +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_ACCESS_TOKEN, CONF_ADDRESS +from homeassistant.data_entry_flow import FlowResult + +from .const import DOMAIN + +_LOGGER: logging.Logger = logging.getLogger(__package__) + + +def short_address(address: str) -> str: + """Convert a Bluetooth address to a short address.""" + results = address.replace("-", ":").split(":") + return f"{results[0].upper()}{results[1].upper()}"[0:4] + + +def name_from_discovery(discovery: MicroBotAdvertisement) -> str: + """Get the name from a discovery.""" + return f'{discovery.data["local_name"]} {short_address(discovery.address)}' + + +class MicroBotConfigFlow(ConfigFlow, domain=DOMAIN): + """Config flow for MicroBot.""" + + VERSION = 1 + + def __init__(self): + """Initialize.""" + self._errors = {} + self._discovered_adv: MicroBotAdvertisement | None = None + self._discovered_advs: dict[str, MicroBotAdvertisement] = {} + self._client: MicroBotApiClient | None = None + self._ble_device: BLEDevice | None = None + self._name: str | None = None + self._bdaddr: str | None = None + + async def async_step_bluetooth( + self, discovery_info: BluetoothServiceInfoBleak + ) -> FlowResult: + """Handle the bluetooth discovery step.""" + _LOGGER.debug("Discovered bluetooth device: %s", discovery_info) + await self.async_set_unique_id(discovery_info.address) + self._abort_if_unique_id_configured() + self._ble_device = discovery_info.device + parsed = parse_advertisement_data( + discovery_info.device, discovery_info.advertisement + ) + self._discovered_adv = parsed + self.context["title_placeholders"] = { + "name": name_from_discovery(self._discovered_adv), + } + return await self.async_step_init() + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a flow initialized by the user.""" + # This is for backwards compatibility. + return await self.async_step_init(user_input) + + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Check if paired.""" + errors: dict[str, str] = {} + + if discovery := self._discovered_adv: + self._discovered_advs[discovery.address] = discovery + else: + current_addresses = self._async_current_ids() + for discovery_info in async_discovered_service_info(self.hass): + self._ble_device = discovery_info.device + address = discovery_info.address + if address in current_addresses or address in self._discovered_advs: + continue + parsed = parse_advertisement_data( + discovery_info.device, discovery_info.advertisement + ) + if parsed: + self._discovered_adv = parsed + self._discovered_advs[address] = parsed + + if not self._discovered_advs: + return self.async_abort(reason="no_unconfigured_devices") + + if user_input is not None: + self._name = name_from_discovery(self._discovered_adv) + self._bdaddr = user_input[CONF_ADDRESS] + await self.async_set_unique_id(self._bdaddr, raise_on_progress=False) + self._abort_if_unique_id_configured() + return await self.async_step_link() + + return self.async_show_form( + step_id="init", + data_schema=vol.Schema( + { + vol.Required(CONF_ADDRESS): vol.In( + { + address: f"{parsed.data['local_name']} ({address})" + for address, parsed in self._discovered_advs.items() + } + ) + } + ), + errors=errors, + ) + + async def async_step_link( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Given a configured host, will ask the user to press the button to pair.""" + errors: dict[str, str] = {} + token = randomid(32) + self._client = MicroBotApiClient( + device=self._ble_device, + token=token, + ) + assert self._client is not None + if user_input is None: + await self._client.connect(init=True) + return self.async_show_form(step_id="link") + + if not self._client.is_connected(): + errors["base"] = "linking" + else: + await self._client.disconnect() + + if errors: + return self.async_show_form(step_id="link", errors=errors) + + assert self._name is not None + return self.async_create_entry( + title=self._name, + data=user_input + | { + CONF_ADDRESS: self._bdaddr, + CONF_ACCESS_TOKEN: token, + }, + ) diff --git a/homeassistant/components/keymitt_ble/const.py b/homeassistant/components/keymitt_ble/const.py new file mode 100644 index 00000000000..a10e7124226 --- /dev/null +++ b/homeassistant/components/keymitt_ble/const.py @@ -0,0 +1,4 @@ +"""Constants for Keymitt BLE.""" +# Base component constants +DOMAIN = "keymitt_ble" +MANUFACTURER = "Naran/Keymitt" diff --git a/homeassistant/components/keymitt_ble/coordinator.py b/homeassistant/components/keymitt_ble/coordinator.py new file mode 100644 index 00000000000..e3a995e3813 --- /dev/null +++ b/homeassistant/components/keymitt_ble/coordinator.py @@ -0,0 +1,56 @@ +"""Integration to integrate Keymitt BLE devices with Home Assistant.""" +from __future__ import annotations + +import logging +from typing import TYPE_CHECKING, Any + +from microbot import MicroBotApiClient, parse_advertisement_data + +from homeassistant.components import bluetooth +from homeassistant.components.bluetooth.passive_update_coordinator import ( + PassiveBluetoothDataUpdateCoordinator, +) +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant, callback + +if TYPE_CHECKING: + from bleak.backends.device import BLEDevice + +_LOGGER: logging.Logger = logging.getLogger(__package__) +PLATFORMS: list[str] = [Platform.SWITCH] + + +class MicroBotDataUpdateCoordinator(PassiveBluetoothDataUpdateCoordinator): + """Class to manage fetching data from the MicroBot.""" + + def __init__( + self, + hass: HomeAssistant, + client: MicroBotApiClient, + ble_device: BLEDevice, + ) -> None: + """Initialize.""" + self.api: MicroBotApiClient = client + self.data: dict[str, Any] = {} + self.ble_device = ble_device + super().__init__( + hass, + _LOGGER, + ble_device.address, + bluetooth.BluetoothScanningMode.ACTIVE, + ) + + @callback + def _async_handle_bluetooth_event( + self, + service_info: bluetooth.BluetoothServiceInfoBleak, + change: bluetooth.BluetoothChange, + ) -> None: + """Handle a Bluetooth event.""" + if adv := parse_advertisement_data( + service_info.device, service_info.advertisement + ): + self.data = adv.data + _LOGGER.debug("%s: MicroBot data: %s", self.ble_device.address, self.data) + self.api.update_from_advertisement(adv) + super()._async_handle_bluetooth_event(service_info, change) diff --git a/homeassistant/components/keymitt_ble/entity.py b/homeassistant/components/keymitt_ble/entity.py new file mode 100644 index 00000000000..dcda4a94027 --- /dev/null +++ b/homeassistant/components/keymitt_ble/entity.py @@ -0,0 +1,39 @@ +"""MicroBot class.""" +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +from homeassistant.components.bluetooth.passive_update_coordinator import ( + PassiveBluetoothCoordinatorEntity, +) +from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.entity import DeviceInfo + +from .const import MANUFACTURER + +if TYPE_CHECKING: + from . import MicroBotDataUpdateCoordinator + + +class MicroBotEntity(PassiveBluetoothCoordinatorEntity): + """Generic entity for all MicroBots.""" + + coordinator: MicroBotDataUpdateCoordinator + + def __init__(self, coordinator, config_entry): + """Initialise the entity.""" + super().__init__(coordinator) + self._address = self.coordinator.ble_device.address + self._attr_name = "Push" + self._attr_unique_id = self._address + self._attr_device_info = DeviceInfo( + connections={(dr.CONNECTION_BLUETOOTH, self._address)}, + manufacturer=MANUFACTURER, + model="Push", + name="MicroBot", + ) + + @property + def data(self) -> dict[str, Any]: + """Return coordinator data for this entity.""" + return self.coordinator.data diff --git a/homeassistant/components/keymitt_ble/manifest.json b/homeassistant/components/keymitt_ble/manifest.json new file mode 100644 index 00000000000..445a2581bda --- /dev/null +++ b/homeassistant/components/keymitt_ble/manifest.json @@ -0,0 +1,22 @@ +{ + "domain": "keymitt_ble", + "name": "Keymitt MicroBot Push", + "documentation": "https://www.home-assistant.io/integrations/keymitt_ble", + "config_flow": true, + "bluetooth": [ + { + "service_uuid": "00001831-0000-1000-8000-00805f9b34fb" + }, + { + "service_data_uuid": "00001831-0000-1000-8000-00805f9b34fb" + }, + { + "local_name": "mib*" + } + ], + "codeowners": ["@spycle"], + "requirements": ["PyMicroBot==0.0.6"], + "iot_class": "assumed_state", + "dependencies": ["bluetooth"], + "loggers": ["keymitt_ble"] +} diff --git a/homeassistant/components/keymitt_ble/services.yaml b/homeassistant/components/keymitt_ble/services.yaml new file mode 100644 index 00000000000..c611577eb26 --- /dev/null +++ b/homeassistant/components/keymitt_ble/services.yaml @@ -0,0 +1,46 @@ +calibrate: + name: Calibrate + description: Calibration - Set depth, press & hold duration, and operation mode. Warning - this will send a push command to the device + fields: + entity_id: + name: Entity + description: Name of entity to calibrate + selector: + entity: + integration: keymitt_ble + domain: switch + depth: + name: Depth + description: Depth in percent + example: 50 + required: true + selector: + number: + mode: slider + step: 1 + min: 0 + max: 100 + unit_of_measurement: "%" + duration: + name: Duration + description: Duration in seconds + example: 1 + required: true + selector: + number: + mode: box + step: 1 + min: 0 + max: 60 + unit_of_measurement: seconds + mode: + name: Mode + description: normal | invert | toggle + example: "normal" + required: true + selector: + select: + options: + - "normal" + - "invert" + - "toggle" diff --git a/homeassistant/components/keymitt_ble/strings.json b/homeassistant/components/keymitt_ble/strings.json new file mode 100644 index 00000000000..3914a2f9a30 --- /dev/null +++ b/homeassistant/components/keymitt_ble/strings.json @@ -0,0 +1,27 @@ +{ + "config": { + "flow_title": "{name}", + "step": { + "init": { + "title": "Setup MicroBot device", + "data": { + "address": "Device address", + "name": "Name" + } + }, + "link": { + "title": "Pairing", + "description": "Press the button on the MicroBot Push when the LED is solid pink or green to register with Home Assistant." + } + }, + "error": { + "linking": "Failed to pair, please try again. Is the MicroBot in pairing mode?" + }, + "abort": { + "already_configured_device": "[%key:common::config_flow::abort::already_configured_device%]", + "no_unconfigured_devices": "No unconfigured devices found.", + "unknown": "[%key:common::config_flow::error::unknown%]", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + } + } +} diff --git a/homeassistant/components/keymitt_ble/switch.py b/homeassistant/components/keymitt_ble/switch.py new file mode 100644 index 00000000000..92decea53ca --- /dev/null +++ b/homeassistant/components/keymitt_ble/switch.py @@ -0,0 +1,70 @@ +"""Switch platform for MicroBot.""" +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +import voluptuous as vol + +from homeassistant.components.switch import SwitchEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers import config_validation as cv, entity_platform + +from .const import DOMAIN +from .entity import MicroBotEntity + +if TYPE_CHECKING: + from . import MicroBotDataUpdateCoordinator + +CALIBRATE = "calibrate" +CALIBRATE_SCHEMA = { + vol.Required("depth"): cv.positive_int, + vol.Required("duration"): cv.positive_int, + vol.Required("mode"): vol.In(["normal", "invert", "toggle"]), +} + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: entity_platform.AddEntitiesCallback, +) -> None: + """Set up MicroBot based on a config entry.""" + coordinator: MicroBotDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + async_add_entities([MicroBotBinarySwitch(coordinator, entry)]) + platform = entity_platform.async_get_current_platform() + platform.async_register_entity_service( + CALIBRATE, + CALIBRATE_SCHEMA, + "async_calibrate", + ) + + +class MicroBotBinarySwitch(MicroBotEntity, SwitchEntity): + """MicroBot switch class.""" + + _attr_has_entity_name = True + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn on the switch.""" + await self.coordinator.api.push_on() + self.async_write_ha_state() + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn off the switch.""" + await self.coordinator.api.push_off() + self.async_write_ha_state() + + @property + def is_on(self) -> bool: + """Return true if the switch is on.""" + return self.coordinator.api.is_on + + async def async_calibrate( + self, + depth: int, + duration: int, + mode: str, + ) -> None: + """Send calibration commands to the switch.""" + await self.coordinator.api.calibrate(depth, duration, mode) diff --git a/homeassistant/components/keymitt_ble/translations/bg.json b/homeassistant/components/keymitt_ble/translations/bg.json new file mode 100644 index 00000000000..4f10191a3d1 --- /dev/null +++ b/homeassistant/components/keymitt_ble/translations/bg.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured_device": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "no_unconfigured_devices": "\u041d\u044f\u043c\u0430 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 \u043d\u0435\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0438 \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" + }, + "flow_title": "{name}", + "step": { + "init": { + "data": { + "address": "\u0410\u0434\u0440\u0435\u0441 \u043d\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e", + "name": "\u0418\u043c\u0435" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/keymitt_ble/translations/ca.json b/homeassistant/components/keymitt_ble/translations/ca.json new file mode 100644 index 00000000000..afae4bbd458 --- /dev/null +++ b/homeassistant/components/keymitt_ble/translations/ca.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured_device": "El dispositiu ja est\u00e0 configurat", + "cannot_connect": "Ha fallat la connexi\u00f3", + "no_unconfigured_devices": "No s'han trobat dispositius no configurats.", + "unknown": "Error inesperat" + }, + "error": { + "linking": "No s'ha pogut vincular, torna-ho a provar. El MicroBot est\u00e0 en mode vinculaci\u00f3?" + }, + "flow_title": "{name}", + "step": { + "init": { + "data": { + "address": "Adre\u00e7a del dispositiu", + "name": "Nom" + }, + "title": "Configuraci\u00f3 de dispositiu MicroBot" + }, + "link": { + "description": "Prem el bot\u00f3 del MicroBot Push quan el LED estigui enc\u00e8s de color rosa o verd per registrar-lo a Home Assistant.", + "title": "Vinculaci\u00f3" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/keymitt_ble/translations/de.json b/homeassistant/components/keymitt_ble/translations/de.json new file mode 100644 index 00000000000..a03d9c725be --- /dev/null +++ b/homeassistant/components/keymitt_ble/translations/de.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured_device": "Ger\u00e4t ist bereits konfiguriert", + "cannot_connect": "Verbindung fehlgeschlagen", + "no_unconfigured_devices": "Keine unkonfigurierten Ger\u00e4te gefunden.", + "unknown": "Unerwarteter Fehler" + }, + "error": { + "linking": "Pairing fehlgeschlagen, bitte versuche es erneut. Befindet sich der MicroBot im Kopplungsmodus?" + }, + "flow_title": "{name}", + "step": { + "init": { + "data": { + "address": "Ger\u00e4teadresse", + "name": "Name" + }, + "title": "MicroBot-Ger\u00e4t einrichten" + }, + "link": { + "description": "Dr\u00fccke die Taste am MicroBot Push, wenn die LED durchgehend rosa oder gr\u00fcn leuchtet, um sich bei Home Assistant zu registrieren.", + "title": "Kopplung" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/keymitt_ble/translations/el.json b/homeassistant/components/keymitt_ble/translations/el.json new file mode 100644 index 00000000000..bb6521f4b36 --- /dev/null +++ b/homeassistant/components/keymitt_ble/translations/el.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured_device": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "no_unconfigured_devices": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03bc\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03bc\u03ad\u03bd\u03b5\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2.", + "unknown": "\u0391\u03c0\u03c1\u03bf\u03c3\u03b4\u03cc\u03ba\u03b7\u03c4\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, + "error": { + "linking": "\u0397 \u03b1\u03bd\u03c4\u03b9\u03c3\u03c4\u03bf\u03af\u03c7\u03b9\u03c3\u03b7 \u03b1\u03c0\u03ad\u03c4\u03c5\u03c7\u03b5, \u03c0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03ce \u03b4\u03bf\u03ba\u03b9\u03bc\u03ac\u03c3\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac. \u0395\u03af\u03bd\u03b1\u03b9 \u03c4\u03bf MicroBot \u03c3\u03b5 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03b1\u03bd\u03c4\u03b9\u03c3\u03c4\u03bf\u03af\u03c7\u03b9\u03c3\u03b7\u03c2;" + }, + "flow_title": "{name}", + "step": { + "init": { + "data": { + "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" + }, + "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.", + "title": "\u0391\u03bd\u03c4\u03b9\u03c3\u03c4\u03bf\u03af\u03c7\u03b9\u03c3\u03b7" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/keymitt_ble/translations/en.json b/homeassistant/components/keymitt_ble/translations/en.json new file mode 100644 index 00000000000..ca5fa547770 --- /dev/null +++ b/homeassistant/components/keymitt_ble/translations/en.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured_device": "Device is already configured", + "cannot_connect": "Failed to connect", + "no_unconfigured_devices": "No unconfigured devices found.", + "unknown": "Unexpected error" + }, + "error": { + "linking": "Failed to pair, please try again. Is the MicroBot in pairing mode?" + }, + "flow_title": "{name}", + "step": { + "init": { + "data": { + "address": "Device address", + "name": "Name" + }, + "title": "Setup MicroBot device" + }, + "link": { + "description": "Press the button on the MicroBot Push when the LED is solid pink or green to register with Home Assistant.", + "title": "Pairing" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/keymitt_ble/translations/es.json b/homeassistant/components/keymitt_ble/translations/es.json new file mode 100644 index 00000000000..62b31234780 --- /dev/null +++ b/homeassistant/components/keymitt_ble/translations/es.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured_device": "El dispositivo ya est\u00e1 configurado", + "cannot_connect": "No se pudo conectar", + "no_unconfigured_devices": "No se encontraron dispositivos no configurados.", + "unknown": "Error inesperado" + }, + "error": { + "linking": "No se pudo emparejar, por favor, int\u00e9ntalo de nuevo. \u00bfEst\u00e1 el MicroBot en modo de emparejamiento?" + }, + "flow_title": "{name}", + "step": { + "init": { + "data": { + "address": "Direcci\u00f3n del dispositivo", + "name": "Nombre" + }, + "title": "Configurar el dispositivo MicroBot" + }, + "link": { + "description": "Pulsa el bot\u00f3n en el MicroBot Push cuando el LED est\u00e9 fijo en rosa o verde para registrarlo con Home Assistant.", + "title": "Emparejamiento" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/keymitt_ble/translations/fr.json b/homeassistant/components/keymitt_ble/translations/fr.json new file mode 100644 index 00000000000..2b425195d11 --- /dev/null +++ b/homeassistant/components/keymitt_ble/translations/fr.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured_device": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "cannot_connect": "\u00c9chec de connexion", + "no_unconfigured_devices": "Aucun appareil non configur\u00e9 n'a \u00e9t\u00e9 trouv\u00e9.", + "unknown": "Erreur inattendue" + }, + "error": { + "linking": "\u00c9chec de l'appairage, veuillez r\u00e9essayer. Le MicroBot est-il en mode d'appairage\u00a0?" + }, + "flow_title": "{name}", + "step": { + "init": { + "data": { + "address": "Adresse de l'appareil", + "name": "Nom" + }, + "title": "Configurer l'appareil MicroBot" + }, + "link": { + "title": "Appairage" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/keymitt_ble/translations/he.json b/homeassistant/components/keymitt_ble/translations/he.json new file mode 100644 index 00000000000..b02c2388374 --- /dev/null +++ b/homeassistant/components/keymitt_ble/translations/he.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "already_configured_device": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "flow_title": "{name}" + } +} \ No newline at end of file diff --git a/homeassistant/components/keymitt_ble/translations/hu.json b/homeassistant/components/keymitt_ble/translations/hu.json new file mode 100644 index 00000000000..0792bf4ad23 --- /dev/null +++ b/homeassistant/components/keymitt_ble/translations/hu.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured_device": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "no_unconfigured_devices": "Nem tal\u00e1lhat\u00f3 konfigur\u00e1latlan eszk\u00f6z.", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "error": { + "linking": "Nem siker\u00fclt p\u00e1ros\u00edtani, k\u00e9rem, pr\u00f3b\u00e1lja \u00fajra. A MicroBot p\u00e1ros\u00edt\u00e1si m\u00f3dban van?" + }, + "flow_title": "{name}", + "step": { + "init": { + "data": { + "address": "Eszk\u00f6z c\u00edme", + "name": "N\u00e9v" + }, + "title": "MicroBot eszk\u00f6z be\u00e1ll\u00edt\u00e1sa" + }, + "link": { + "description": "Nyomja meg a MicroBoton a Push gombot, amikor a LED r\u00f3zsasz\u00edn vagy z\u00f6ld sz\u00ednnel vil\u00e1g\u00edt, hogy az regisztr\u00e1l\u00f3djon a Home Assistant rendszerbe.", + "title": "P\u00e1ros\u00edt\u00e1s" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/keymitt_ble/translations/id.json b/homeassistant/components/keymitt_ble/translations/id.json new file mode 100644 index 00000000000..e94cd17c5c8 --- /dev/null +++ b/homeassistant/components/keymitt_ble/translations/id.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured_device": "Perangkat sudah dikonfigurasi", + "cannot_connect": "Gagal terhubung", + "no_unconfigured_devices": "Tidak ditemukan perangkat yang tidak dikonfigurasi.", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "error": { + "linking": "Gagal memasangkan, silakan coba lagi. Apakah MicroBot dalam mode pairing?" + }, + "flow_title": "{name}", + "step": { + "init": { + "data": { + "address": "Alamat perangkat", + "name": "Nama" + }, + "title": "Siapkan perangkat MicroBot" + }, + "link": { + "description": "Tekan tombol pada MicroBot Push ketika LED berwarna merah muda atau hijau untuk mendaftarkannya ke Home Assistant.", + "title": "Pemasangan" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/keymitt_ble/translations/it.json b/homeassistant/components/keymitt_ble/translations/it.json new file mode 100644 index 00000000000..583f4d3940b --- /dev/null +++ b/homeassistant/components/keymitt_ble/translations/it.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured_device": "Il dispositivo \u00e8 gi\u00e0 configurato", + "cannot_connect": "Impossibile connettersi", + "no_unconfigured_devices": "Non sono stati trovati dispositivi non configurati.", + "unknown": "Errore imprevisto" + }, + "error": { + "linking": "Impossibile eseguire l'associazione, riprovare. Il MicroBot \u00e8 in modalit\u00e0 di associazione?" + }, + "flow_title": "{name}", + "step": { + "init": { + "data": { + "address": "Indirizzo del dispositivo", + "name": "Nome" + }, + "title": "Configura il dispositivo MicroBot" + }, + "link": { + "description": "Premere il pulsante sul MicroBot Push quando il LED \u00e8 rosa o verde fisso per registrarsi con Home Assistant.", + "title": "Abbinamento" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/keymitt_ble/translations/nl.json b/homeassistant/components/keymitt_ble/translations/nl.json new file mode 100644 index 00000000000..853de9222ba --- /dev/null +++ b/homeassistant/components/keymitt_ble/translations/nl.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured_device": "Apparaat is al geconfigureerd", + "cannot_connect": "Kan geen verbinding maken", + "no_unconfigured_devices": "Geen niet-geconfigureerde apparaten gevonden.", + "unknown": "Onverwachte fout" + }, + "flow_title": "{name}", + "step": { + "init": { + "data": { + "name": "Naam" + }, + "title": "MicroBot-apparaat instellen" + }, + "link": { + "title": "Koppelen" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/keymitt_ble/translations/no.json b/homeassistant/components/keymitt_ble/translations/no.json new file mode 100644 index 00000000000..6aa5ca36d43 --- /dev/null +++ b/homeassistant/components/keymitt_ble/translations/no.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured_device": "Enheten er allerede konfigurert", + "cannot_connect": "Tilkobling mislyktes", + "no_unconfigured_devices": "Fant ingen ukonfigurerte enheter.", + "unknown": "Uventet feil" + }, + "error": { + "linking": "Kunne ikke pare. Pr\u00f8v igjen. Er MicroBot i sammenkoblingsmodus?" + }, + "flow_title": "{name}", + "step": { + "init": { + "data": { + "address": "Enhetsadresse", + "name": "Navn" + }, + "title": "Sett opp MicroBot-enhet" + }, + "link": { + "description": "Trykk p\u00e5 knappen p\u00e5 MicroBot Push n\u00e5r lysdioden lyser rosa eller gr\u00f8nt for \u00e5 registrere deg med Home Assistant.", + "title": "Sammenkobling" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/keymitt_ble/translations/pl.json b/homeassistant/components/keymitt_ble/translations/pl.json new file mode 100644 index 00000000000..4f7f8f59a1c --- /dev/null +++ b/homeassistant/components/keymitt_ble/translations/pl.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured_device": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "no_unconfigured_devices": "Nie znaleziono nieskonfigurowanych urz\u0105dze\u0144.", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "error": { + "linking": "Nie uda\u0142o si\u0119 sparowa\u0107, spr\u00f3buj ponownie. Czy MicroBot jest w trybie parowania?" + }, + "flow_title": "{name}", + "step": { + "init": { + "data": { + "address": "Adres urz\u0105dzenia", + "name": "Nazwa" + }, + "title": "Konfiguracja urz\u0105dzenia MicroBot" + }, + "link": { + "description": "Naci\u015bnij przycisk na MicroBot Push, gdy dioda LED \u015bwieci na r\u00f3\u017cowo lub zielono, aby zarejestrowa\u0107 go w Home Assistant.", + "title": "Parowanie" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/keymitt_ble/translations/pt-BR.json b/homeassistant/components/keymitt_ble/translations/pt-BR.json new file mode 100644 index 00000000000..66e44612afe --- /dev/null +++ b/homeassistant/components/keymitt_ble/translations/pt-BR.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured_device": "O dispositivo j\u00e1 est\u00e1 configurado", + "cannot_connect": "Falhou ao conectar", + "no_unconfigured_devices": "Nenhum dispositivo n\u00e3o configurado encontrado.", + "unknown": "Erro inesperado" + }, + "error": { + "linking": "Falha ao emparelhar. Tente novamente. O MicroBot est\u00e1 no modo de emparelhamento?" + }, + "flow_title": "{name}", + "step": { + "init": { + "data": { + "address": "Endere\u00e7o do dispositivo", + "name": "Nome" + }, + "title": "Configurar dispositivo MicroBot" + }, + "link": { + "description": "Pressione o bot\u00e3o no MicroBot Push quando o LED estiver rosa ou verde s\u00f3lido para se registrar no Home Assistant.", + "title": "Emparelhamento" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/keymitt_ble/translations/ru.json b/homeassistant/components/keymitt_ble/translations/ru.json new file mode 100644 index 00000000000..200bd4d798c --- /dev/null +++ b/homeassistant/components/keymitt_ble/translations/ru.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured_device": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "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.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "error": { + "linking": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435. \u041f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443. \u041d\u0430\u0445\u043e\u0434\u0438\u0442\u0441\u044f \u043b\u0438 MicroBot \u0432 \u0440\u0435\u0436\u0438\u043c\u0435 \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u044f?" + }, + "flow_title": "{name}", + "step": { + "init": { + "data": { + "address": "\u0410\u0434\u0440\u0435\u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" + }, + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 MicroBot" + }, + "link": { + "description": "\u041d\u0430\u0436\u043c\u0438\u0442\u0435 \u043a\u043d\u043e\u043f\u043a\u0443 \u043d\u0430 MicroBot Push, \u043a\u043e\u0433\u0434\u0430 \u0438\u043d\u0434\u0438\u043a\u0430\u0442\u043e\u0440 \u0433\u043e\u0440\u0438\u0442 \u0440\u043e\u0437\u043e\u0432\u044b\u043c \u0438\u043b\u0438 \u0437\u0435\u043b\u0435\u043d\u044b\u043c \u0446\u0432\u0435\u0442\u043e\u043c, \u0447\u0442\u043e\u0431\u044b \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0432 Home Assistant.", + "title": "\u0421\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/keymitt_ble/translations/zh-Hant.json b/homeassistant/components/keymitt_ble/translations/zh-Hant.json new file mode 100644 index 00000000000..7f48cd5e633 --- /dev/null +++ b/homeassistant/components/keymitt_ble/translations/zh-Hant.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured_device": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "no_unconfigured_devices": "\u627e\u4e0d\u5230\u4efb\u4f55\u672a\u8a2d\u5b9a\u88dd\u7f6e\u3002", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "error": { + "linking": "\u914d\u5c0d\u5931\u6557\uff0c\u8acb\u518d\u8a66\u4e00\u6b21\u3002MicroBot \u662f\u5426\u8655\u65bc\u914d\u5c0d\u6a21\u5f0f\uff1f" + }, + "flow_title": "{name}", + "step": { + "init": { + "data": { + "address": "\u88dd\u7f6e\u4f4d\u5740", + "name": "\u540d\u7a31" + }, + "title": "\u8a2d\u5b9a MicroBot \u88dd\u7f6e" + }, + "link": { + "description": "\u6309\u4f4f MicroBot \u4e0a\u7684\u6309\u9215\u76f4\u5230\u5e38\u4eae\u7c89\u8272\u6216\u7da0\u8272\uff0c\u4ee5\u8a3b\u518a\u81f3 Home Assistant\u3002", + "title": "\u914d\u5c0d\u4e2d" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kira/remote.py b/homeassistant/components/kira/remote.py index 37be3792aa9..4c06216a210 100644 --- a/homeassistant/components/kira/remote.py +++ b/homeassistant/components/kira/remote.py @@ -1,13 +1,13 @@ """Support for Keene Electronics IR-IP devices.""" from __future__ import annotations -import functools as ft +from collections.abc import Iterable import logging +from typing import Any from homeassistant.components import remote from homeassistant.const import CONF_DEVICE, CONF_NAME from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -31,32 +31,18 @@ def setup_platform( add_entities([KiraRemote(device, kira)]) -class KiraRemote(Entity): +class KiraRemote(remote.RemoteEntity): """Remote representation used to send commands to a Kira device.""" def __init__(self, name, kira): """Initialize KiraRemote class.""" _LOGGER.debug("KiraRemote device init started for: %s", name) - self._name = name + self._attr_name = name self._kira = kira - @property - def name(self): - """Return the Kira device's name.""" - return self._name - - def update(self): - """No-op.""" - - def send_command(self, command, **kwargs): + def send_command(self, command: Iterable[str], **kwargs: Any) -> None: """Send a command to one device.""" for single_command in command: code_tuple = (single_command, kwargs.get(remote.ATTR_DEVICE)) _LOGGER.info("Sending Command: %s to %s", *code_tuple) self._kira.sendCode(code_tuple) - - async def async_send_command(self, command, **kwargs): - """Send a command to a device.""" - return await self.hass.async_add_executor_job( - ft.partial(self.send_command, command, **kwargs) - ) diff --git a/homeassistant/components/kira/sensor.py b/homeassistant/components/kira/sensor.py index d4488781849..e1a4f08dd14 100644 --- a/homeassistant/components/kira/sensor.py +++ b/homeassistant/components/kira/sensor.py @@ -13,8 +13,6 @@ from . import CONF_SENSOR, DOMAIN _LOGGER = logging.getLogger(__name__) -ICON = "mdi:remote" - def setup_platform( hass: HomeAssistant, @@ -34,44 +32,20 @@ def setup_platform( class KiraReceiver(SensorEntity): """Implementation of a Kira Receiver.""" + _attr_force_update = True # repeated states have meaning in Kira + _attr_icon = "mdi:remote" _attr_should_poll = False def __init__(self, name, kira): """Initialize the sensor.""" - self._name = name - self._state = None - self._device = STATE_UNKNOWN + self._attr_name = name + self._attr_extra_state_attributes = {CONF_DEVICE: STATE_UNKNOWN} kira.registerCallback(self._update_callback) def _update_callback(self, code): code_name, device = code _LOGGER.debug("Kira Code: %s", code_name) - self._state = code_name - self._device = device + self._attr_native_value = code_name + self._attr_extra_state_attributes[CONF_DEVICE] = device self.schedule_update_ha_state() - - @property - def name(self): - """Return the name of the receiver.""" - return self._name - - @property - def icon(self): - """Return icon.""" - return ICON - - @property - def native_value(self): - """Return the state of the receiver.""" - return self._state - - @property - def extra_state_attributes(self): - """Return the state attributes of the device.""" - return {CONF_DEVICE: self._device} - - @property - def force_update(self) -> bool: - """Kira should force updates. Repeated states have meaning.""" - return True diff --git a/homeassistant/components/kmtronic/switch.py b/homeassistant/components/kmtronic/switch.py index e941a2ffafa..860e5bf832e 100644 --- a/homeassistant/components/kmtronic/switch.py +++ b/homeassistant/components/kmtronic/switch.py @@ -1,4 +1,5 @@ """KMtronic Switch integration.""" +from typing import Any import urllib.parse from homeassistant.components.switch import SwitchEntity @@ -54,7 +55,7 @@ class KMtronicSwitch(CoordinatorEntity, SwitchEntity): return not self._relay.is_energised return self._relay.is_energised - async def async_turn_on(self, **kwargs) -> None: + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" if self._reverse: await self._relay.de_energise() @@ -62,7 +63,7 @@ class KMtronicSwitch(CoordinatorEntity, SwitchEntity): await self._relay.energise() self.async_write_ha_state() - async def async_turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the switch off.""" if self._reverse: await self._relay.energise() @@ -70,7 +71,7 @@ class KMtronicSwitch(CoordinatorEntity, SwitchEntity): await self._relay.de_energise() self.async_write_ha_state() - async def async_toggle(self, **kwargs) -> None: + async def async_toggle(self, **kwargs: Any) -> None: """Toggle the switch.""" await self._relay.toggle() self.async_write_ha_state() diff --git a/homeassistant/components/knx/climate.py b/homeassistant/components/knx/climate.py index 2e1fd61ad43..bb2fe7dfbcc 100644 --- a/homeassistant/components/knx/climate.py +++ b/homeassistant/components/knx/climate.py @@ -8,9 +8,9 @@ from xknx.devices import Climate as XknxClimate, ClimateMode as XknxClimateMode from xknx.dpt.dpt_hvac_mode import HVACControllerMode, HVACOperationMode from homeassistant import config_entries -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( PRESET_AWAY, + ClimateEntity, ClimateEntityFeature, HVACAction, HVACMode, diff --git a/homeassistant/components/knx/const.py b/homeassistant/components/knx/const.py index df8f0de3216..3cc73a6dd35 100644 --- a/homeassistant/components/knx/const.py +++ b/homeassistant/components/knx/const.py @@ -4,7 +4,7 @@ from __future__ import annotations from enum import Enum from typing import Final, TypedDict -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( PRESET_AWAY, PRESET_COMFORT, PRESET_ECO, diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index c0aa6c3941c..0f2f1201415 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -3,7 +3,7 @@ "name": "KNX", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/knx", - "requirements": ["xknx==1.0.2"], + "requirements": ["xknx==1.1.0"], "codeowners": ["@Julius2342", "@farmio", "@marvin-w"], "quality_scale": "platinum", "iot_class": "local_push", diff --git a/homeassistant/components/knx/schema.py b/homeassistant/components/knx/schema.py index c7c1e264975..55602d6153a 100644 --- a/homeassistant/components/knx/schema.py +++ b/homeassistant/components/knx/schema.py @@ -16,7 +16,7 @@ from xknx.telegram.address import IndividualAddress, parse_device_group_address from homeassistant.components.binary_sensor import ( DEVICE_CLASSES_SCHEMA as BINARY_SENSOR_DEVICE_CLASSES_SCHEMA, ) -from homeassistant.components.climate.const import HVACMode +from homeassistant.components.climate import HVACMode from homeassistant.components.cover import ( DEVICE_CLASSES_SCHEMA as COVER_DEVICE_CLASSES_SCHEMA, ) diff --git a/homeassistant/components/knx/translations/cs.json b/homeassistant/components/knx/translations/cs.json index 31c65f915dd..90c988aaeac 100644 --- a/homeassistant/components/knx/translations/cs.json +++ b/homeassistant/components/knx/translations/cs.json @@ -3,9 +3,13 @@ "abort": { "already_configured": "Slu\u017eba je ji\u017e nastavena" }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" + }, "step": { "manual_tunnel": { "data": { + "host": "Hostitel", "port": "Port" } } @@ -15,6 +19,7 @@ "step": { "tunnel": { "data": { + "host": "Hostitel", "port": "Port" } } diff --git a/homeassistant/components/knx/translations/he.json b/homeassistant/components/knx/translations/he.json index ef11ad342ee..dea454b5f6c 100644 --- a/homeassistant/components/knx/translations/he.json +++ b/homeassistant/components/knx/translations/he.json @@ -16,7 +16,8 @@ }, "routing": { "data": { - "multicast_group": "\u05e7\u05d1\u05d5\u05e6\u05ea \u05e9\u05d9\u05d3\u05d5\u05e8 \u05de\u05e8\u05d5\u05d1\u05d4 \u05d4\u05de\u05e9\u05de\u05e9\u05ea \u05dc\u05e0\u05d9\u05ea\u05d5\u05d1" + "multicast_group": "\u05e7\u05d1\u05d5\u05e6\u05ea \u05e9\u05d9\u05d3\u05d5\u05e8 \u05dc\u05e7\u05d1\u05d5\u05e6\u05d4", + "multicast_port": "\u05d9\u05e6\u05d9\u05d0\u05ea \u05e9\u05d9\u05d3\u05d5\u05e8 \u05dc\u05e7\u05d1\u05d5\u05e6\u05d4" } } } @@ -25,7 +26,8 @@ "step": { "init": { "data": { - "multicast_group": "\u05e7\u05d1\u05d5\u05e6\u05ea \u05e9\u05d9\u05d3\u05d5\u05e8 \u05de\u05e8\u05d5\u05d1\u05d4 \u05d4\u05de\u05e9\u05de\u05e9\u05ea \u05dc\u05e0\u05d9\u05ea\u05d5\u05d1 \u05d5\u05d2\u05d9\u05dc\u05d5\u05d9" + "multicast_group": "\u05e7\u05d1\u05d5\u05e6\u05ea \u05e9\u05d9\u05d3\u05d5\u05e8 \u05dc\u05e7\u05d1\u05d5\u05e6\u05d4", + "multicast_port": "\u05d9\u05e6\u05d9\u05d0\u05ea \u05e9\u05d9\u05d3\u05d5\u05e8 \u05dc\u05e7\u05d1\u05d5\u05e6\u05d4" } }, "tunnel": { diff --git a/homeassistant/components/knx/translations/ja.json b/homeassistant/components/knx/translations/ja.json index bbac3566bca..3272508a525 100644 --- a/homeassistant/components/knx/translations/ja.json +++ b/homeassistant/components/knx/translations/ja.json @@ -61,13 +61,13 @@ "user_id": "\u591a\u304f\u306e\u5834\u5408\u3001\u3053\u308c\u306f\u30c8\u30f3\u30cd\u30eb\u756a\u53f7+1\u3067\u3059\u3002\u3057\u305f\u304c\u3063\u3066\u3001 '\u30c8\u30f3\u30cd\u30eb2' \u306e\u30e6\u30fc\u30b6\u30fcID\u306f\u3001'3 '\u306b\u306a\u308a\u307e\u3059\u3002", "user_password": "ETS\u306e\u30c8\u30f3\u30cd\u30eb\u306e\u3001'\u30d7\u30ed\u30d1\u30c6\u30a3' \u30d1\u30cd\u30eb\u3067\u8a2d\u5b9a\u3055\u308c\u305f\u7279\u5b9a\u306e\u30c8\u30f3\u30cd\u30eb\u63a5\u7d9a\u7528\u306e\u30d1\u30b9\u30ef\u30fc\u30c9\u3002" }, - "description": "IP\u30bb\u30ad\u30e5\u30a2\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + "description": "IP \u30bb\u30ad\u30e5\u30a2\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, "secure_tunneling": { - "description": "IP\u30bb\u30ad\u30e5\u30a2\u306e\u8a2d\u5b9a\u65b9\u6cd5\u3092\u9078\u629e\u3057\u307e\u3059\u3002", + "description": "KNX/IP \u30bb\u30ad\u30e5\u30a2\u3092\u69cb\u6210\u3059\u308b\u65b9\u6cd5\u3092\u9078\u629e\u3057\u307e\u3059\u3002", "menu_options": { - "secure_knxkeys": "IP\u30bb\u30ad\u30e5\u30a2\u60c5\u5831\u3092\u542b\u3080knxkeys\u30d5\u30a1\u30a4\u30eb\u3092\u8a2d\u5b9a\u3057\u307e\u3059", - "secure_manual": "IP\u30bb\u30ad\u30e5\u30a2\u3092\u624b\u52d5\u3067\u8a2d\u5b9a\u3059\u308b" + "secure_knxkeys": "IP \u30bb\u30ad\u30e5\u30a2 \u30ad\u30fc\u3092\u542b\u3080\u300c.knxkeys\u300d\u30d5\u30a1\u30a4\u30eb\u3092\u4f7f\u7528\u3059\u308b", + "secure_manual": "IP \u30bb\u30ad\u30e5\u30a2 \u30ad\u30fc\u3092\u624b\u52d5\u3067\u69cb\u6210\u3059\u308b" } }, "tunnel": { @@ -102,7 +102,7 @@ "multicast_group": "\u30eb\u30fc\u30c6\u30a3\u30f3\u30b0\u3068\u691c\u51fa\u306b\u4f7f\u7528\u3055\u308c\u307e\u3059\u3002\u30c7\u30d5\u30a9\u30eb\u30c8t: `224.0.23.12`", "multicast_port": "\u30eb\u30fc\u30c6\u30a3\u30f3\u30b0\u3068\u691c\u51fa\u306b\u4f7f\u7528\u3055\u308c\u307e\u3059\u3002\u30c7\u30d5\u30a9\u30eb\u30c8: `3671`", "rate_limit": "1\u79d2\u3042\u305f\u308a\u306e\u6700\u5927\u9001\u4fe1\u30c6\u30ec\u30b0\u30e9\u30e0\u3002\n\u63a8\u5968: 20\uff5e40", - "state_updater": "KNX Bus\u304b\u3089\u306e\u72b6\u614b\u306e\u8aad\u307f\u53d6\u308a\u3092\u30b0\u30ed\u30fc\u30d0\u30eb\u306b\u6709\u52b9\u307e\u305f\u306f\u7121\u52b9\u306b\u3057\u307e\u3059\u3002\u7121\u52b9\u306b\u3059\u308b\u3068\u3001Home Assistant\u306f\u3001KNX Bus\u304b\u3089\u30a2\u30af\u30c6\u30a3\u30d6\u306b\u72b6\u614b\u3092\u53d6\u5f97\u3057\u306a\u304f\u306a\u308b\u306e\u3067\u3001`sync_state`\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u30aa\u30d7\u30b7\u30e7\u30f3\u306f\u610f\u5473\u3092\u6301\u305f\u306a\u304f\u306a\u308a\u307e\u3059\u3002" + "state_updater": "KNX \u30d0\u30b9\u304b\u3089\u72b6\u614b\u3092\u8aad\u307f\u53d6\u308b\u305f\u3081\u306e\u30c7\u30d5\u30a9\u30eb\u30c8\u3092\u8a2d\u5b9a\u3057\u307e\u3059\u3002\u7121\u52b9\u306b\u3059\u308b\u3068\u3001Home Assistant \u306f KNX \u30d0\u30b9\u304b\u3089\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u306e\u72b6\u614b\u3092\u7a4d\u6975\u7684\u306b\u53d6\u5f97\u3057\u307e\u305b\u3093\u3002 \u300csync_state\u300d\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3 \u30aa\u30d7\u30b7\u30e7\u30f3\u3067\u30aa\u30fc\u30d0\u30fc\u30e9\u30a4\u30c9\u3067\u304d\u307e\u3059\u3002" } }, "tunnel": { diff --git a/homeassistant/components/kodi/browse_media.py b/homeassistant/components/kodi/browse_media.py index 73247d23a9d..cfb4fa3caa6 100644 --- a/homeassistant/components/kodi/browse_media.py +++ b/homeassistant/components/kodi/browse_media.py @@ -4,54 +4,37 @@ import contextlib import logging from homeassistant.components import media_source -from homeassistant.components.media_player import BrowseError, BrowseMedia -from homeassistant.components.media_player.const import ( - MEDIA_CLASS_ALBUM, - MEDIA_CLASS_ARTIST, - MEDIA_CLASS_CHANNEL, - MEDIA_CLASS_DIRECTORY, - MEDIA_CLASS_EPISODE, - MEDIA_CLASS_MOVIE, - MEDIA_CLASS_MUSIC, - MEDIA_CLASS_PLAYLIST, - MEDIA_CLASS_SEASON, - MEDIA_CLASS_TRACK, - MEDIA_CLASS_TV_SHOW, - MEDIA_TYPE_ALBUM, - MEDIA_TYPE_ARTIST, - MEDIA_TYPE_CHANNEL, - MEDIA_TYPE_EPISODE, - MEDIA_TYPE_MOVIE, - MEDIA_TYPE_PLAYLIST, - MEDIA_TYPE_SEASON, - MEDIA_TYPE_TRACK, - MEDIA_TYPE_TVSHOW, +from homeassistant.components.media_player import ( + BrowseError, + BrowseMedia, + MediaClass, + MediaType, ) PLAYABLE_MEDIA_TYPES = [ - MEDIA_TYPE_ALBUM, - MEDIA_TYPE_ARTIST, - MEDIA_TYPE_TRACK, + MediaType.ALBUM, + MediaType.ARTIST, + MediaType.TRACK, ] CONTAINER_TYPES_SPECIFIC_MEDIA_CLASS = { - MEDIA_TYPE_ALBUM: MEDIA_CLASS_ALBUM, - MEDIA_TYPE_ARTIST: MEDIA_CLASS_ARTIST, - MEDIA_TYPE_PLAYLIST: MEDIA_CLASS_PLAYLIST, - MEDIA_TYPE_SEASON: MEDIA_CLASS_SEASON, - MEDIA_TYPE_TVSHOW: MEDIA_CLASS_TV_SHOW, + MediaType.ALBUM: MediaClass.ALBUM, + MediaType.ARTIST: MediaClass.ARTIST, + MediaType.PLAYLIST: MediaClass.PLAYLIST, + MediaType.SEASON: MediaClass.SEASON, + MediaType.TVSHOW: MediaClass.TV_SHOW, } CHILD_TYPE_MEDIA_CLASS = { - MEDIA_TYPE_SEASON: MEDIA_CLASS_SEASON, - MEDIA_TYPE_ALBUM: MEDIA_CLASS_ALBUM, - MEDIA_TYPE_ARTIST: MEDIA_CLASS_ARTIST, - MEDIA_TYPE_MOVIE: MEDIA_CLASS_MOVIE, - MEDIA_TYPE_PLAYLIST: MEDIA_CLASS_PLAYLIST, - MEDIA_TYPE_TRACK: MEDIA_CLASS_TRACK, - MEDIA_TYPE_TVSHOW: MEDIA_CLASS_TV_SHOW, - MEDIA_TYPE_CHANNEL: MEDIA_CLASS_CHANNEL, - MEDIA_TYPE_EPISODE: MEDIA_CLASS_EPISODE, + MediaType.SEASON: MediaClass.SEASON, + MediaType.ALBUM: MediaClass.ALBUM, + MediaType.ARTIST: MediaClass.ARTIST, + MediaType.MOVIE: MediaClass.MOVIE, + MediaType.PLAYLIST: MediaClass.PLAYLIST, + MediaType.TRACK: MediaClass.TRACK, + MediaType.TVSHOW: MediaClass.TV_SHOW, + MediaType.CHANNEL: MediaClass.CHANNEL, + MediaType.EPISODE: MediaClass.EPISODE, } _LOGGER = logging.getLogger(__name__) @@ -76,12 +59,12 @@ async def build_item_response(media_library, payload, get_thumbnail_url=None): *(item_payload(item, get_thumbnail_url) for item in media) ) - if search_type in (MEDIA_TYPE_TVSHOW, MEDIA_TYPE_MOVIE) and search_id == "": + if search_type in (MediaType.TVSHOW, MediaType.MOVIE) and search_id == "": children.sort(key=lambda x: x.title.replace("The ", "", 1), reverse=False) response = BrowseMedia( media_class=CONTAINER_TYPES_SPECIFIC_MEDIA_CLASS.get( - search_type, MEDIA_CLASS_DIRECTORY + search_type, MediaClass.DIRECTORY ), media_content_id=search_id, media_content_type=search_type, @@ -93,7 +76,7 @@ async def build_item_response(media_library, payload, get_thumbnail_url=None): ) if search_type == "library_music": - response.children_media_class = MEDIA_CLASS_MUSIC + response.children_media_class = MediaClass.MUSIC else: response.calculate_children_class() @@ -111,42 +94,42 @@ async def item_payload(item, get_thumbnail_url=None): media_class = None if "songid" in item: - media_content_type = MEDIA_TYPE_TRACK + media_content_type = MediaType.TRACK media_content_id = f"{item['songid']}" can_play = True can_expand = False elif "albumid" in item: - media_content_type = MEDIA_TYPE_ALBUM + media_content_type = MediaType.ALBUM media_content_id = f"{item['albumid']}" can_play = True can_expand = True elif "artistid" in item: - media_content_type = MEDIA_TYPE_ARTIST + media_content_type = MediaType.ARTIST media_content_id = f"{item['artistid']}" can_play = True can_expand = True elif "movieid" in item: - media_content_type = MEDIA_TYPE_MOVIE + media_content_type = MediaType.MOVIE media_content_id = f"{item['movieid']}" can_play = True can_expand = False elif "episodeid" in item: - media_content_type = MEDIA_TYPE_EPISODE + media_content_type = MediaType.EPISODE media_content_id = f"{item['episodeid']}" can_play = True can_expand = False elif "seasonid" in item: - media_content_type = MEDIA_TYPE_SEASON + media_content_type = MediaType.SEASON media_content_id = f"{item['tvshowid']}/{item['season']}" can_play = False can_expand = True elif "tvshowid" in item: - media_content_type = MEDIA_TYPE_TVSHOW + media_content_type = MediaType.TVSHOW media_content_id = f"{item['tvshowid']}" can_play = False can_expand = True elif "channelid" in item: - media_content_type = MEDIA_TYPE_CHANNEL + media_content_type = MediaType.CHANNEL media_content_id = f"{item['channelid']}" if broadcasting := item.get("broadcastnow"): show = broadcasting.get("title") @@ -156,7 +139,7 @@ async def item_payload(item, get_thumbnail_url=None): else: # this case is for the top folder of each type # possible content types: album, artist, movie, library_music, tvshow, channel - media_class = MEDIA_CLASS_DIRECTORY + media_class = MediaClass.DIRECTORY media_content_type = item["type"] media_content_id = "" can_play = False @@ -202,7 +185,7 @@ async def library_payload(hass): Used by async_browse_media. """ library_info = BrowseMedia( - media_class=MEDIA_CLASS_DIRECTORY, + media_class=MediaClass.DIRECTORY, media_content_id="library", media_content_type="library", title="Media Library", @@ -213,9 +196,9 @@ async def library_payload(hass): library = { "library_music": "Music", - MEDIA_TYPE_MOVIE: "Movies", - MEDIA_TYPE_TVSHOW: "TV shows", - MEDIA_TYPE_CHANNEL: "Channels", + MediaType.MOVIE: "Movies", + MediaType.TVSHOW: "TV shows", + MediaType.CHANNEL: "Channels", } library_info.children = await asyncio.gather( @@ -256,7 +239,7 @@ async def get_media_info(media_library, search_id, search_type): media = None properties = ["thumbnail"] - if search_type == MEDIA_TYPE_ALBUM: + if search_type == MediaType.ALBUM: if search_id: album = await media_library.get_album_details( album_id=int(search_id), properties=properties @@ -282,7 +265,7 @@ async def get_media_info(media_library, search_id, search_type): media = media.get("albums") title = "Albums" - elif search_type == MEDIA_TYPE_ARTIST: + elif search_type == MediaType.ARTIST: if search_id: media = await media_library.get_albums( artist_id=int(search_id), properties=properties @@ -301,11 +284,11 @@ async def get_media_info(media_library, search_id, search_type): title = "Artists" elif search_type == "library_music": - library = {MEDIA_TYPE_ALBUM: "Albums", MEDIA_TYPE_ARTIST: "Artists"} + library = {MediaType.ALBUM: "Albums", MediaType.ARTIST: "Artists"} media = [{"label": name, "type": type_} for type_, name in library.items()] title = "Music Library" - elif search_type == MEDIA_TYPE_MOVIE: + elif search_type == MediaType.MOVIE: if search_id: movie = await media_library.get_movie_details( movie_id=int(search_id), properties=properties @@ -319,7 +302,7 @@ async def get_media_info(media_library, search_id, search_type): media = media.get("movies") title = "Movies" - elif search_type == MEDIA_TYPE_TVSHOW: + elif search_type == MediaType.TVSHOW: if search_id: media = await media_library.get_seasons( tv_show_id=int(search_id), @@ -338,7 +321,7 @@ async def get_media_info(media_library, search_id, search_type): media = media.get("tvshows") title = "TV Shows" - elif search_type == MEDIA_TYPE_SEASON: + elif search_type == MediaType.SEASON: tv_show_id, season_id = search_id.split("/", 1) media = await media_library.get_episodes( tv_show_id=int(tv_show_id), @@ -355,7 +338,7 @@ async def get_media_info(media_library, search_id, search_type): ) title = season["seasondetails"]["label"] - elif search_type == MEDIA_TYPE_CHANNEL: + elif search_type == MediaType.CHANNEL: media = await media_library.get_channels( channel_group_id="alltv", properties=["thumbnail", "channeltype", "channel", "broadcastnow"], diff --git a/homeassistant/components/kodi/media_player.py b/homeassistant/components/kodi/media_player.py index 2b509ed0e08..bdbac455dd1 100644 --- a/homeassistant/components/kodi/media_player.py +++ b/homeassistant/components/kodi/media_player.py @@ -7,7 +7,6 @@ from functools import wraps import logging import re from typing import Any, TypeVar -import urllib.parse from jsonrpc_base.jsonrpc import ProtocolError, TransportError from pykodi import CannotConnectError @@ -17,27 +16,14 @@ import voluptuous as vol from homeassistant.components import media_source from homeassistant.components.media_player import ( PLATFORM_SCHEMA, + BrowseError, + BrowseMedia, MediaPlayerEntity, MediaPlayerEntityFeature, -) -from homeassistant.components.media_player.browse_media import ( + MediaPlayerState, + MediaType, async_process_play_media_url, ) -from homeassistant.components.media_player.const import ( - MEDIA_TYPE_ALBUM, - MEDIA_TYPE_ARTIST, - MEDIA_TYPE_CHANNEL, - MEDIA_TYPE_EPISODE, - MEDIA_TYPE_MOVIE, - MEDIA_TYPE_MUSIC, - MEDIA_TYPE_PLAYLIST, - MEDIA_TYPE_SEASON, - MEDIA_TYPE_TRACK, - MEDIA_TYPE_TVSHOW, - MEDIA_TYPE_URL, - MEDIA_TYPE_VIDEO, -) -from homeassistant.components.media_player.errors import BrowseError from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( ATTR_ENTITY_ID, @@ -50,10 +36,6 @@ from homeassistant.const import ( CONF_TIMEOUT, CONF_USERNAME, EVENT_HOMEASSISTANT_STARTED, - STATE_IDLE, - STATE_OFF, - STATE_PAUSED, - STATE_PLAYING, ) from homeassistant.core import CoreState, HomeAssistant, callback from homeassistant.helpers import ( @@ -112,28 +94,28 @@ WEBSOCKET_WATCHDOG_INTERVAL = timedelta(seconds=10) # https://github.com/xbmc/xbmc/blob/master/xbmc/media/MediaType.h MEDIA_TYPES = { - "music": MEDIA_TYPE_MUSIC, - "artist": MEDIA_TYPE_MUSIC, - "album": MEDIA_TYPE_MUSIC, - "song": MEDIA_TYPE_MUSIC, - "video": MEDIA_TYPE_VIDEO, - "set": MEDIA_TYPE_PLAYLIST, - "musicvideo": MEDIA_TYPE_VIDEO, - "movie": MEDIA_TYPE_MOVIE, - "tvshow": MEDIA_TYPE_TVSHOW, - "season": MEDIA_TYPE_TVSHOW, - "episode": MEDIA_TYPE_TVSHOW, + "music": MediaType.MUSIC, + "artist": MediaType.MUSIC, + "album": MediaType.MUSIC, + "song": MediaType.MUSIC, + "video": MediaType.VIDEO, + "set": MediaType.PLAYLIST, + "musicvideo": MediaType.VIDEO, + "movie": MediaType.MOVIE, + "tvshow": MediaType.TVSHOW, + "season": MediaType.TVSHOW, + "episode": MediaType.TVSHOW, # Type 'channel' is used for radio or tv streams from pvr - "channel": MEDIA_TYPE_CHANNEL, + "channel": MediaType.CHANNEL, # Type 'audio' is used for audio media, that Kodi couldn't scroblle - "audio": MEDIA_TYPE_MUSIC, + "audio": MediaType.MUSIC, } -MAP_KODI_MEDIA_TYPES = { - MEDIA_TYPE_MOVIE: "movieid", - MEDIA_TYPE_EPISODE: "episodeid", - MEDIA_TYPE_SEASON: "seasonid", - MEDIA_TYPE_TVSHOW: "tvshowid", +MAP_KODI_MEDIA_TYPES: dict[MediaType | str, str] = { + MediaType.MOVIE: "movieid", + MediaType.EPISODE: "episodeid", + MediaType.SEASON: "seasonid", + MediaType.TVSHOW: "tvshowid", } @@ -259,7 +241,7 @@ def cmd( await func(obj, *args, **kwargs) except (TransportError, ProtocolError) as exc: # If Kodi is off, we expect calls to fail. - if obj.state == STATE_OFF: + if obj.state == MediaPlayerState.OFF: log_function = _LOGGER.debug else: log_function = _LOGGER.error @@ -380,20 +362,20 @@ class KodiEntity(MediaPlayerEntity): ) @property - def state(self): + def state(self) -> MediaPlayerState: """Return the state of the device.""" if self._kodi_is_off: - return STATE_OFF + return MediaPlayerState.OFF if self._no_active_players: - return STATE_IDLE + return MediaPlayerState.IDLE if self._properties["speed"] == 0: - return STATE_PAUSED + return MediaPlayerState.PAUSED - return STATE_PLAYING + return MediaPlayerState.PLAYING - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Connect the websocket if needed.""" if not self._connection.can_subscribe: return @@ -481,7 +463,7 @@ class KodiEntity(MediaPlayerEntity): self._connection.server.System.OnSleep = self.async_on_quit @cmd - async def async_update(self): + async def async_update(self) -> None: """Retrieve latest state.""" if not self._connection.connected: self._reset_state() @@ -526,7 +508,7 @@ class KodiEntity(MediaPlayerEntity): self._reset_state([]) @property - def should_poll(self): + def should_poll(self) -> bool: """Return True if entity has to be polled for state.""" return not self._connection.can_subscribe @@ -636,78 +618,78 @@ class KodiEntity(MediaPlayerEntity): return None - async def async_turn_on(self): + async def async_turn_on(self) -> None: """Turn the media player on.""" _LOGGER.debug("Firing event to turn on device") self.hass.bus.async_fire(EVENT_TURN_ON, {ATTR_ENTITY_ID: self.entity_id}) - async def async_turn_off(self): + async def async_turn_off(self) -> None: """Turn the media player off.""" _LOGGER.debug("Firing event to turn off device") self.hass.bus.async_fire(EVENT_TURN_OFF, {ATTR_ENTITY_ID: self.entity_id}) @cmd - async def async_volume_up(self): + async def async_volume_up(self) -> None: """Volume up the media player.""" await self._kodi.volume_up() @cmd - async def async_volume_down(self): + async def async_volume_down(self) -> None: """Volume down the media player.""" await self._kodi.volume_down() @cmd - async def async_set_volume_level(self, volume): + async def async_set_volume_level(self, volume: float) -> None: """Set volume level, range 0..1.""" await self._kodi.set_volume_level(int(volume * 100)) @cmd - async def async_mute_volume(self, mute): + async def async_mute_volume(self, mute: bool) -> None: """Mute (true) or unmute (false) media player.""" await self._kodi.mute(mute) @cmd - async def async_media_play_pause(self): + async def async_media_play_pause(self) -> None: """Pause media on media player.""" await self._kodi.play_pause() @cmd - async def async_media_play(self): + async def async_media_play(self) -> None: """Play media.""" await self._kodi.play() @cmd - async def async_media_pause(self): + async def async_media_pause(self) -> None: """Pause the media player.""" await self._kodi.pause() @cmd - async def async_media_stop(self): + async def async_media_stop(self) -> None: """Stop the media player.""" await self._kodi.stop() @cmd - async def async_media_next_track(self): + async def async_media_next_track(self) -> None: """Send next track command.""" await self._kodi.next_track() @cmd - async def async_media_previous_track(self): + async def async_media_previous_track(self) -> None: """Send next track command.""" await self._kodi.previous_track() @cmd - async def async_media_seek(self, position): + async def async_media_seek(self, position: float) -> None: """Send seek command.""" await self._kodi.media_seek(position) @cmd 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_media command to the media player.""" if media_source.is_media_source_id(media_id): - media_type = MEDIA_TYPE_URL + media_type = MediaType.URL play_item = await media_source.async_resolve_media( self.hass, media_id, self.entity_id ) @@ -715,27 +697,27 @@ class KodiEntity(MediaPlayerEntity): media_type_lower = media_type.lower() - if media_type_lower == MEDIA_TYPE_CHANNEL: + if media_type_lower == MediaType.CHANNEL: await self._kodi.play_channel(int(media_id)) - elif media_type_lower == MEDIA_TYPE_PLAYLIST: + elif media_type_lower == MediaType.PLAYLIST: await self._kodi.play_playlist(int(media_id)) elif media_type_lower == "file": await self._kodi.play_file(media_id) elif media_type_lower == "directory": await self._kodi.play_directory(media_id) elif media_type_lower in [ - MEDIA_TYPE_ARTIST, - MEDIA_TYPE_ALBUM, - MEDIA_TYPE_TRACK, + MediaType.ARTIST, + MediaType.ALBUM, + MediaType.TRACK, ]: await self.async_clear_playlist() await self.async_add_to_playlist(media_type_lower, media_id) await self._kodi.play_playlist(0) elif media_type_lower in [ - MEDIA_TYPE_MOVIE, - MEDIA_TYPE_EPISODE, - MEDIA_TYPE_SEASON, - MEDIA_TYPE_TVSHOW, + MediaType.MOVIE, + MediaType.EPISODE, + MediaType.SEASON, + MediaType.TVSHOW, ]: await self._kodi.play_item( {MAP_KODI_MEDIA_TYPES[media_type_lower]: int(media_id)} @@ -746,7 +728,7 @@ class KodiEntity(MediaPlayerEntity): await self._kodi.play_file(media_id) @cmd - async def async_set_shuffle(self, shuffle): + async def async_set_shuffle(self, shuffle: bool) -> None: """Set shuffle mode, for the first player.""" if self._no_active_players: raise RuntimeError("Error: No active player.") @@ -790,17 +772,17 @@ class KodiEntity(MediaPlayerEntity): ) return result - async def async_clear_playlist(self): + async def async_clear_playlist(self) -> None: """Clear default playlist (i.e. playlistid=0).""" await self._kodi.clear_playlist() async def async_add_to_playlist(self, media_type, media_id): """Add media item to default playlist (i.e. playlistid=0).""" - if media_type == MEDIA_TYPE_ARTIST: + if media_type == MediaType.ARTIST: await self._kodi.add_artist_to_playlist(int(media_id)) - elif media_type == MEDIA_TYPE_ALBUM: + elif media_type == MediaType.ALBUM: await self._kodi.add_album_to_playlist(int(media_id)) - elif media_type == MEDIA_TYPE_TRACK: + elif media_type == MediaType.TRACK: await self._kodi.add_song_to_playlist(int(media_id)) async def async_add_media_to_playlist( @@ -902,7 +884,9 @@ class KodiEntity(MediaPlayerEntity): return sorted(out, key=lambda out: out[1], reverse=True) - 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.""" is_internal = is_internal_request(self.hass) @@ -917,14 +901,14 @@ class KodiEntity(MediaPlayerEntity): return self.get_browse_image_url( media_content_type, - urllib.parse.quote_plus(media_content_id), + media_content_id, media_image_id, ) if media_content_type in [None, "library"]: return await library_payload(self.hass) - if media_source.is_media_source_id(media_content_id): + if media_content_id and media_source.is_media_source_id(media_content_id): return await media_source.async_browse_media( self.hass, media_content_id, content_filter=media_source_content_filter ) diff --git a/homeassistant/components/kodi/translations/zh-Hant.json b/homeassistant/components/kodi/translations/zh-Hant.json index 169ad92e96b..7e72914d672 100644 --- a/homeassistant/components/kodi/translations/zh-Hant.json +++ b/homeassistant/components/kodi/translations/zh-Hant.json @@ -23,7 +23,7 @@ }, "discovery_confirm": { "description": "\u662f\u5426\u8981\u65b0\u589e Kodi (`{name}`) \u81f3 Home Assistant\uff1f", - "title": "\u5df2\u767c\u73fe\u7684 Kodi" + "title": "\u81ea\u52d5\u641c\u7d22\u5230\u7684 Kodi" }, "user": { "data": { diff --git a/homeassistant/components/konnected/binary_sensor.py b/homeassistant/components/konnected/binary_sensor.py index e1823c1c7d9..a4ceed5c50d 100644 --- a/homeassistant/components/konnected/binary_sensor.py +++ b/homeassistant/components/konnected/binary_sensor.py @@ -76,7 +76,7 @@ class KonnectedBinarySensor(BinarySensorEntity): identifiers={(KONNECTED_DOMAIN, self._device_id)}, ) - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Store entity_id and register state change callback.""" self._data[ATTR_ENTITY_ID] = self.entity_id self.async_on_remove( diff --git a/homeassistant/components/konnected/sensor.py b/homeassistant/components/konnected/sensor.py index 3bd3a05c609..7bfa1fad446 100644 --- a/homeassistant/components/konnected/sensor.py +++ b/homeassistant/components/konnected/sensor.py @@ -127,7 +127,7 @@ class KonnectedSensor(SensorEntity): """Return the state of the sensor.""" return self._state - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Store entity_id and register state change callback.""" entity_id_key = self._addr or self.entity_description.key self._data[entity_id_key] = self.entity_id diff --git a/homeassistant/components/konnected/switch.py b/homeassistant/components/konnected/switch.py index 123c5b94ab4..c5a0ca712e5 100644 --- a/homeassistant/components/konnected/switch.py +++ b/homeassistant/components/konnected/switch.py @@ -1,5 +1,6 @@ """Support for wired switches attached to a Konnected device.""" import logging +from typing import Any from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry @@ -89,11 +90,11 @@ class KonnectedSwitch(SwitchEntity): return DeviceInfo(identifiers={(KONNECTED_DOMAIN, self._device_id)}) @property - def available(self): + def available(self) -> bool: """Return whether the panel is available.""" return self.panel.available - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Send a command to turn on the switch.""" resp = await self.panel.update_switch( self._zone_num, @@ -110,7 +111,7 @@ class KonnectedSwitch(SwitchEntity): # Immediately set the state back off for momentary switches self._set_state(False) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Send a command to turn off the switch.""" resp = await self.panel.update_switch( self._zone_num, int(self._activation == STATE_LOW) @@ -142,7 +143,7 @@ class KonnectedSwitch(SwitchEntity): """Update the switch state.""" self._set_state(state) - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Store entity_id and register state change callback.""" self._data["entity_id"] = self.entity_id self.async_on_remove( diff --git a/homeassistant/components/konnected/translations/cs.json b/homeassistant/components/konnected/translations/cs.json index f2cd8759a54..621ae847e34 100644 --- a/homeassistant/components/konnected/translations/cs.json +++ b/homeassistant/components/konnected/translations/cs.json @@ -3,6 +3,7 @@ "abort": { "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", "already_in_progress": "Konfigurace ji\u017e prob\u00edh\u00e1", + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", "not_konn_panel": "Nejedn\u00e1 se o rozpoznan\u00e9 Konnected.io za\u0159\u00edzen\u00ed", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, diff --git a/homeassistant/components/kostal_plenticore/const.py b/homeassistant/components/kostal_plenticore/const.py index 7ae0b13f0e8..c0e897e6131 100644 --- a/homeassistant/components/kostal_plenticore/const.py +++ b/homeassistant/components/kostal_plenticore/const.py @@ -1,857 +1,2 @@ """Constants for the Kostal Plenticore Solar Inverter integration.""" -from typing import NamedTuple - -from homeassistant.components.sensor import ( - ATTR_STATE_CLASS, - SensorDeviceClass, - SensorStateClass, -) -from homeassistant.const import ( - ATTR_DEVICE_CLASS, - ATTR_ICON, - ATTR_UNIT_OF_MEASUREMENT, - ELECTRIC_CURRENT_AMPERE, - ELECTRIC_POTENTIAL_VOLT, - ENERGY_KILO_WATT_HOUR, - PERCENTAGE, - POWER_WATT, -) - DOMAIN = "kostal_plenticore" - -ATTR_ENABLED_DEFAULT = "entity_registry_enabled_default" - -# Defines all entities for process data. -# -# Each entry is defined with a tuple of these values: -# - module id (str) -# - process data id (str) -# - entity name suffix (str) -# - sensor properties (dict) -# - value formatter (str) -SENSOR_PROCESS_DATA = [ - ( - "devices:local", - "Inverter:State", - "Inverter State", - {ATTR_ICON: "mdi:state-machine"}, - "format_inverter_state", - ), - ( - "devices:local", - "Dc_P", - "Solar Power", - { - ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, - ATTR_DEVICE_CLASS: SensorDeviceClass.POWER, - ATTR_ENABLED_DEFAULT: True, - ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, - }, - "format_round", - ), - ( - "devices:local", - "Grid_P", - "Grid Power", - { - ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, - ATTR_DEVICE_CLASS: SensorDeviceClass.POWER, - ATTR_ENABLED_DEFAULT: True, - ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, - }, - "format_round", - ), - ( - "devices:local", - "HomeBat_P", - "Home Power from Battery", - { - ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, - ATTR_DEVICE_CLASS: SensorDeviceClass.POWER, - }, - "format_round", - ), - ( - "devices:local", - "HomeGrid_P", - "Home Power from Grid", - { - ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, - ATTR_DEVICE_CLASS: SensorDeviceClass.POWER, - ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, - }, - "format_round", - ), - ( - "devices:local", - "HomeOwn_P", - "Home Power from Own", - { - ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, - ATTR_DEVICE_CLASS: SensorDeviceClass.POWER, - ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, - }, - "format_round", - ), - ( - "devices:local", - "HomePv_P", - "Home Power from PV", - { - ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, - ATTR_DEVICE_CLASS: SensorDeviceClass.POWER, - ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, - }, - "format_round", - ), - ( - "devices:local", - "Home_P", - "Home Power", - { - ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, - ATTR_DEVICE_CLASS: SensorDeviceClass.POWER, - ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, - }, - "format_round", - ), - ( - "devices:local:ac", - "P", - "AC Power", - { - ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, - ATTR_DEVICE_CLASS: SensorDeviceClass.POWER, - ATTR_ENABLED_DEFAULT: True, - ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, - }, - "format_round", - ), - ( - "devices:local:pv1", - "P", - "DC1 Power", - { - ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, - ATTR_DEVICE_CLASS: SensorDeviceClass.POWER, - ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, - }, - "format_round", - ), - ( - "devices:local:pv1", - "U", - "DC1 Voltage", - { - ATTR_UNIT_OF_MEASUREMENT: ELECTRIC_POTENTIAL_VOLT, - ATTR_DEVICE_CLASS: SensorDeviceClass.VOLTAGE, - ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, - }, - "format_round", - ), - ( - "devices:local:pv1", - "I", - "DC1 Current", - { - ATTR_UNIT_OF_MEASUREMENT: ELECTRIC_CURRENT_AMPERE, - ATTR_DEVICE_CLASS: SensorDeviceClass.CURRENT, - ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, - }, - "format_float", - ), - ( - "devices:local:pv2", - "P", - "DC2 Power", - { - ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, - ATTR_DEVICE_CLASS: SensorDeviceClass.POWER, - ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, - }, - "format_round", - ), - ( - "devices:local:pv2", - "U", - "DC2 Voltage", - { - ATTR_UNIT_OF_MEASUREMENT: ELECTRIC_POTENTIAL_VOLT, - ATTR_DEVICE_CLASS: SensorDeviceClass.VOLTAGE, - ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, - }, - "format_round", - ), - ( - "devices:local:pv2", - "I", - "DC2 Current", - { - ATTR_UNIT_OF_MEASUREMENT: ELECTRIC_CURRENT_AMPERE, - ATTR_DEVICE_CLASS: SensorDeviceClass.CURRENT, - ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, - }, - "format_float", - ), - ( - "devices:local:pv3", - "P", - "DC3 Power", - { - ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, - ATTR_DEVICE_CLASS: SensorDeviceClass.POWER, - ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, - }, - "format_round", - ), - ( - "devices:local:pv3", - "U", - "DC3 Voltage", - { - ATTR_UNIT_OF_MEASUREMENT: ELECTRIC_POTENTIAL_VOLT, - ATTR_DEVICE_CLASS: SensorDeviceClass.VOLTAGE, - ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, - }, - "format_round", - ), - ( - "devices:local:pv3", - "I", - "DC3 Current", - { - ATTR_UNIT_OF_MEASUREMENT: ELECTRIC_CURRENT_AMPERE, - ATTR_DEVICE_CLASS: SensorDeviceClass.CURRENT, - ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, - }, - "format_float", - ), - ( - "devices:local", - "PV2Bat_P", - "PV to Battery Power", - { - ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, - ATTR_DEVICE_CLASS: SensorDeviceClass.POWER, - ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, - }, - "format_round", - ), - ( - "devices:local", - "EM_State", - "Energy Manager State", - {ATTR_ICON: "mdi:state-machine"}, - "format_em_manager_state", - ), - ( - "devices:local:battery", - "Cycles", - "Battery Cycles", - {ATTR_ICON: "mdi:recycle", ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT}, - "format_round", - ), - ( - "devices:local:battery", - "P", - "Battery Power", - { - ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, - ATTR_DEVICE_CLASS: SensorDeviceClass.POWER, - ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, - }, - "format_round", - ), - ( - "devices:local:battery", - "SoC", - "Battery SoC", - { - ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, - ATTR_DEVICE_CLASS: SensorDeviceClass.BATTERY, - }, - "format_round", - ), - ( - "scb:statistic:EnergyFlow", - "Statistic:Autarky:Day", - "Autarky Day", - {ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, ATTR_ICON: "mdi:chart-donut"}, - "format_round", - ), - ( - "scb:statistic:EnergyFlow", - "Statistic:Autarky:Month", - "Autarky Month", - {ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, ATTR_ICON: "mdi:chart-donut"}, - "format_round", - ), - ( - "scb:statistic:EnergyFlow", - "Statistic:Autarky:Total", - "Autarky Total", - { - ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, - ATTR_ICON: "mdi:chart-donut", - ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, - }, - "format_round", - ), - ( - "scb:statistic:EnergyFlow", - "Statistic:Autarky:Year", - "Autarky Year", - {ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, ATTR_ICON: "mdi:chart-donut"}, - "format_round", - ), - ( - "scb:statistic:EnergyFlow", - "Statistic:OwnConsumptionRate:Day", - "Own Consumption Rate Day", - {ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, ATTR_ICON: "mdi:chart-donut"}, - "format_round", - ), - ( - "scb:statistic:EnergyFlow", - "Statistic:OwnConsumptionRate:Month", - "Own Consumption Rate Month", - {ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, ATTR_ICON: "mdi:chart-donut"}, - "format_round", - ), - ( - "scb:statistic:EnergyFlow", - "Statistic:OwnConsumptionRate:Total", - "Own Consumption Rate Total", - { - ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, - ATTR_ICON: "mdi:chart-donut", - ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, - }, - "format_round", - ), - ( - "scb:statistic:EnergyFlow", - "Statistic:OwnConsumptionRate:Year", - "Own Consumption Rate Year", - {ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, ATTR_ICON: "mdi:chart-donut"}, - "format_round", - ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyHome:Day", - "Home Consumption Day", - { - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - }, - "format_energy", - ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyHome:Month", - "Home Consumption Month", - { - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - }, - "format_energy", - ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyHome:Year", - "Home Consumption Year", - { - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - }, - "format_energy", - ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyHome:Total", - "Home Consumption Total", - { - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - ATTR_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, - }, - "format_energy", - ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyHomeBat:Day", - "Home Consumption from Battery Day", - { - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - }, - "format_energy", - ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyHomeBat:Month", - "Home Consumption from Battery Month", - { - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - }, - "format_energy", - ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyHomeBat:Year", - "Home Consumption from Battery Year", - { - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - }, - "format_energy", - ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyHomeBat:Total", - "Home Consumption from Battery Total", - { - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - ATTR_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, - }, - "format_energy", - ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyHomeGrid:Day", - "Home Consumption from Grid Day", - { - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - }, - "format_energy", - ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyHomeGrid:Month", - "Home Consumption from Grid Month", - { - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - }, - "format_energy", - ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyHomeGrid:Year", - "Home Consumption from Grid Year", - { - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - }, - "format_energy", - ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyHomeGrid:Total", - "Home Consumption from Grid Total", - { - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - ATTR_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, - }, - "format_energy", - ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyHomePv:Day", - "Home Consumption from PV Day", - { - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - }, - "format_energy", - ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyHomePv:Month", - "Home Consumption from PV Month", - { - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - }, - "format_energy", - ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyHomePv:Year", - "Home Consumption from PV Year", - { - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - }, - "format_energy", - ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyHomePv:Total", - "Home Consumption from PV Total", - { - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - ATTR_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, - }, - "format_energy", - ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyPv1:Day", - "Energy PV1 Day", - { - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - }, - "format_energy", - ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyPv1:Month", - "Energy PV1 Month", - { - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - }, - "format_energy", - ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyPv1:Year", - "Energy PV1 Year", - { - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - }, - "format_energy", - ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyPv1:Total", - "Energy PV1 Total", - { - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - ATTR_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, - }, - "format_energy", - ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyPv2:Day", - "Energy PV2 Day", - { - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - }, - "format_energy", - ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyPv2:Month", - "Energy PV2 Month", - { - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - }, - "format_energy", - ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyPv2:Year", - "Energy PV2 Year", - { - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - }, - "format_energy", - ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyPv2:Total", - "Energy PV2 Total", - { - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - ATTR_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, - }, - "format_energy", - ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyPv3:Day", - "Energy PV3 Day", - { - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - }, - "format_energy", - ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyPv3:Month", - "Energy PV3 Month", - { - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - }, - "format_energy", - ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyPv3:Year", - "Energy PV3 Year", - { - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - }, - "format_energy", - ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyPv3:Total", - "Energy PV3 Total", - { - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - ATTR_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, - }, - "format_energy", - ), - ( - "scb:statistic:EnergyFlow", - "Statistic:Yield:Day", - "Energy Yield Day", - { - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - ATTR_ENABLED_DEFAULT: True, - }, - "format_energy", - ), - ( - "scb:statistic:EnergyFlow", - "Statistic:Yield:Month", - "Energy Yield Month", - { - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - }, - "format_energy", - ), - ( - "scb:statistic:EnergyFlow", - "Statistic:Yield:Year", - "Energy Yield Year", - { - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - }, - "format_energy", - ), - ( - "scb:statistic:EnergyFlow", - "Statistic:Yield:Total", - "Energy Yield Total", - { - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - ATTR_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, - }, - "format_energy", - ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyChargeGrid:Day", - "Battery Charge from Grid Day", - { - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - }, - "format_energy", - ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyChargeGrid:Month", - "Battery Charge from Grid Month", - { - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - }, - "format_energy", - ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyChargeGrid:Year", - "Battery Charge from Grid Year", - { - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - }, - "format_energy", - ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyChargeGrid:Total", - "Battery Charge from Grid Total", - { - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - ATTR_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, - }, - "format_energy", - ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyChargePv:Day", - "Battery Charge from PV Day", - { - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - }, - "format_energy", - ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyChargePv:Month", - "Battery Charge from PV Month", - { - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - }, - "format_energy", - ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyChargePv:Year", - "Battery Charge from PV Year", - { - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - }, - "format_energy", - ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyChargePv:Total", - "Battery Charge from PV Total", - { - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - ATTR_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, - }, - "format_energy", - ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyDischargeGrid:Day", - "Energy Discharge to Grid Day", - { - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - }, - "format_energy", - ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyDischargeGrid:Month", - "Energy Discharge to Grid Month", - { - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - }, - "format_energy", - ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyDischargeGrid:Year", - "Energy Discharge to Grid Year", - { - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - }, - "format_energy", - ), - ( - "scb:statistic:EnergyFlow", - "Statistic:EnergyDischargeGrid:Total", - "Energy Discharge to Grid Total", - { - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, - ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, - ATTR_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, - }, - "format_energy", - ), -] - - -class SwitchData(NamedTuple): - """Representation of a SelectData tuple.""" - - module_id: str - data_id: str - name: str - is_on: str - on_value: str - on_label: str - off_value: str - off_label: str - - -# Defines all entities for switches. -# -# Each entry is defined with a tuple of these values: -# - module id (str) -# - process data id (str) -# - entity name suffix (str) -# - on Value (str) -# - on Label (str) -# - off Value (str) -# - off Label (str) -SWITCH_SETTINGS_DATA = [ - SwitchData( - "devices:local", - "Battery:Strategy", - "Battery Strategy", - "1", - "1", - "Automatic", - "2", - "Automatic economical", - ), -] - - -class SelectData(NamedTuple): - """Representation of a SelectData tuple.""" - - module_id: str - data_id: str - name: str - options: list - is_on: str - - -# Defines all entities for select widgets. -# -# Each entry is defined with a tuple of these values: -# - module id (str) -# - process data id (str) -# - entity name suffix (str) -# - options -# - entity is enabled by default (bool) -SELECT_SETTINGS_DATA = [ - SelectData( - "devices:local", - "battery_charge", - "Battery Charging / Usage mode", - ["None", "Battery:SmartBatteryControl:Enable", "Battery:TimeControl:Enable"], - "1", - ) -] diff --git a/homeassistant/components/kostal_plenticore/select.py b/homeassistant/components/kostal_plenticore/select.py index 7ac06f2ebef..3a2d3445a84 100644 --- a/homeassistant/components/kostal_plenticore/select.py +++ b/homeassistant/components/kostal_plenticore/select.py @@ -1,24 +1,53 @@ """Platform for Kostal Plenticore select widgets.""" from __future__ import annotations -from abc import ABC +from dataclasses import dataclass from datetime import timedelta import logging -from homeassistant.components.select import SelectEntity +from homeassistant.components.select import SelectEntity, SelectEntityDescription from homeassistant.config_entries import ConfigEntry -from homeassistant.const import DEVICE_DEFAULT_NAME from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo, EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import DOMAIN, SELECT_SETTINGS_DATA +from .const import DOMAIN from .helper import Plenticore, SelectDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) +@dataclass +class PlenticoreRequiredKeysMixin: + """A class that describes required properties for plenticore select entities.""" + + module_id: str + options: list[str] + + +@dataclass +class PlenticoreSelectEntityDescription( + SelectEntityDescription, PlenticoreRequiredKeysMixin +): + """A class that describes plenticore select entities.""" + + +SELECT_SETTINGS_DATA = [ + PlenticoreSelectEntityDescription( + module_id="devices:local", + key="battery_charge", + name="Battery Charging / Usage mode", + options=[ + "None", + "Battery:SmartBatteryControl:Enable", + "Battery:TimeControl:Enable", + ], + device_class="kostal_plenticore__battery", + ) +] + + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: @@ -35,69 +64,54 @@ async def async_setup_entry( ) entities = [] - for select in SELECT_SETTINGS_DATA: - if select.module_id not in available_settings_data: + for description in SELECT_SETTINGS_DATA: + if description.module_id not in available_settings_data: continue - needed_data_ids = {data_id for data_id in select.options if data_id != "None"} + needed_data_ids = { + data_id for data_id in description.options if data_id != "None" + } available_data_ids = { - setting.id for setting in available_settings_data[select.module_id] + setting.id for setting in available_settings_data[description.module_id] } if not needed_data_ids <= available_data_ids: continue entities.append( PlenticoreDataSelect( select_data_update_coordinator, + description, entry_id=entry.entry_id, platform_name=entry.title, - device_class="kostal_plenticore__battery", - module_id=select.module_id, - data_id=select.data_id, - name=select.name, - current_option="None", - options=select.options, - is_on=select.is_on, device_info=plenticore.device_info, - unique_id=f"{entry.entry_id}_{select.module_id}", ) ) async_add_entities(entities) -class PlenticoreDataSelect(CoordinatorEntity, SelectEntity, ABC): +class PlenticoreDataSelect(CoordinatorEntity, SelectEntity): """Representation of a Plenticore Select.""" _attr_entity_category = EntityCategory.CONFIG + entity_description: PlenticoreSelectEntityDescription def __init__( self, - coordinator, + coordinator: SelectDataUpdateCoordinator, + description: PlenticoreSelectEntityDescription, entry_id: str, platform_name: str, - device_class: str | None, - module_id: str, - data_id: str, - name: str, - current_option: str | None, - options: list[str], - is_on: str, device_info: DeviceInfo, - unique_id: str, ) -> None: """Create a new Select Entity for Plenticore process data.""" super().__init__(coordinator) + self.entity_description = description self.entry_id = entry_id self.platform_name = platform_name - self._attr_device_class = device_class - self.module_id = module_id - self.data_id = data_id - self._attr_options = options - self.all_options = options - self._attr_current_option = current_option - self._is_on = is_on + self.module_id = description.module_id + self.data_id = description.key + self._attr_options = description.options self._device_info = device_info - self._attr_name = name or DEVICE_DEFAULT_NAME - self._attr_unique_id = unique_id + self._attr_unique_id = f"{entry_id}_{description.module_id}" @property def available(self) -> bool: @@ -112,19 +126,16 @@ class PlenticoreDataSelect(CoordinatorEntity, SelectEntity, ABC): async def async_added_to_hass(self) -> None: """Register this entity on the Update Coordinator.""" await super().async_added_to_hass() - self.coordinator.start_fetch_data( - self.module_id, self.data_id, self.all_options - ) + self.coordinator.start_fetch_data(self.module_id, self.data_id, self.options) async def async_will_remove_from_hass(self) -> None: """Unregister this entity from the Update Coordinator.""" - self.coordinator.stop_fetch_data(self.module_id, self.data_id, self.all_options) + self.coordinator.stop_fetch_data(self.module_id, self.data_id, self.options) await super().async_will_remove_from_hass() async def async_select_option(self, option: str) -> None: """Update the current selected option.""" - self._attr_current_option = option - for all_option in self._attr_options: + for all_option in self.options: if all_option != "None": await self.coordinator.async_write_data( self.module_id, {all_option: "0"} diff --git a/homeassistant/components/kostal_plenticore/sensor.py b/homeassistant/components/kostal_plenticore/sensor.py index f66264e1d7a..29b42e88b50 100644 --- a/homeassistant/components/kostal_plenticore/sensor.py +++ b/homeassistant/components/kostal_plenticore/sensor.py @@ -2,24 +2,688 @@ from __future__ import annotations from collections.abc import Callable +from dataclasses import dataclass from datetime import timedelta import logging from typing import Any -from homeassistant.components.sensor import ATTR_STATE_CLASS, SensorEntity +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT +from homeassistant.const import ( + ELECTRIC_CURRENT_AMPERE, + ELECTRIC_POTENTIAL_VOLT, + ENERGY_KILO_WATT_HOUR, + PERCENTAGE, + POWER_WATT, +) from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo, EntityCategory +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import ATTR_ENABLED_DEFAULT, DOMAIN, SENSOR_PROCESS_DATA +from .const import DOMAIN from .helper import PlenticoreDataFormatter, ProcessDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) +@dataclass +class PlenticoreRequiredKeysMixin: + """A class that describes required properties for plenticore sensor entities.""" + + module_id: str + formatter: str + + +@dataclass +class PlenticoreSensorEntityDescription( + SensorEntityDescription, PlenticoreRequiredKeysMixin +): + """A class that describes plenticore sensor entities.""" + + +SENSOR_PROCESS_DATA = [ + PlenticoreSensorEntityDescription( + module_id="devices:local", + key="Inverter:State", + name="Inverter State", + icon="mdi:state-machine", + formatter="format_inverter_state", + ), + PlenticoreSensorEntityDescription( + module_id="devices:local", + key="Dc_P", + name="Solar Power", + native_unit_of_measurement=POWER_WATT, + device_class=SensorDeviceClass.POWER, + entity_registry_enabled_default=True, + state_class=SensorStateClass.MEASUREMENT, + formatter="format_round", + ), + PlenticoreSensorEntityDescription( + module_id="devices:local", + key="Grid_P", + name="Grid Power", + native_unit_of_measurement=POWER_WATT, + device_class=SensorDeviceClass.POWER, + entity_registry_enabled_default=True, + state_class=SensorStateClass.MEASUREMENT, + formatter="format_round", + ), + PlenticoreSensorEntityDescription( + module_id="devices:local", + key="HomeBat_P", + name="Home Power from Battery", + native_unit_of_measurement=POWER_WATT, + device_class=SensorDeviceClass.POWER, + formatter="format_round", + ), + PlenticoreSensorEntityDescription( + module_id="devices:local", + key="HomeGrid_P", + name="Home Power from Grid", + native_unit_of_measurement=POWER_WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + formatter="format_round", + ), + PlenticoreSensorEntityDescription( + module_id="devices:local", + key="HomeOwn_P", + name="Home Power from Own", + native_unit_of_measurement=POWER_WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + formatter="format_round", + ), + PlenticoreSensorEntityDescription( + module_id="devices:local", + key="HomePv_P", + name="Home Power from PV", + native_unit_of_measurement=POWER_WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + formatter="format_round", + ), + PlenticoreSensorEntityDescription( + module_id="devices:local", + key="Home_P", + name="Home Power", + native_unit_of_measurement=POWER_WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + formatter="format_round", + ), + PlenticoreSensorEntityDescription( + module_id="devices:local:ac", + key="P", + name="AC Power", + native_unit_of_measurement=POWER_WATT, + device_class=SensorDeviceClass.POWER, + entity_registry_enabled_default=True, + state_class=SensorStateClass.MEASUREMENT, + formatter="format_round", + ), + PlenticoreSensorEntityDescription( + module_id="devices:local:pv1", + key="P", + name="DC1 Power", + native_unit_of_measurement=POWER_WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + formatter="format_round", + ), + PlenticoreSensorEntityDescription( + module_id="devices:local:pv1", + key="U", + name="DC1 Voltage", + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, + formatter="format_round", + ), + PlenticoreSensorEntityDescription( + module_id="devices:local:pv1", + key="I", + name="DC1 Current", + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, + formatter="format_float", + ), + PlenticoreSensorEntityDescription( + module_id="devices:local:pv2", + key="P", + name="DC2 Power", + native_unit_of_measurement=POWER_WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + formatter="format_round", + ), + PlenticoreSensorEntityDescription( + module_id="devices:local:pv2", + key="U", + name="DC2 Voltage", + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, + formatter="format_round", + ), + PlenticoreSensorEntityDescription( + module_id="devices:local:pv2", + key="I", + name="DC2 Current", + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, + formatter="format_float", + ), + PlenticoreSensorEntityDescription( + module_id="devices:local:pv3", + key="P", + name="DC3 Power", + native_unit_of_measurement=POWER_WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + formatter="format_round", + ), + PlenticoreSensorEntityDescription( + module_id="devices:local:pv3", + key="U", + name="DC3 Voltage", + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, + formatter="format_round", + ), + PlenticoreSensorEntityDescription( + module_id="devices:local:pv3", + key="I", + name="DC3 Current", + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, + formatter="format_float", + ), + PlenticoreSensorEntityDescription( + module_id="devices:local", + key="PV2Bat_P", + name="PV to Battery Power", + native_unit_of_measurement=POWER_WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + formatter="format_round", + ), + PlenticoreSensorEntityDescription( + module_id="devices:local", + key="EM_State", + name="Energy Manager State", + icon="mdi:state-machine", + formatter="format_em_manager_state", + ), + PlenticoreSensorEntityDescription( + module_id="devices:local:battery", + key="Cycles", + name="Battery Cycles", + icon="mdi:recycle", + state_class=SensorStateClass.MEASUREMENT, + formatter="format_round", + ), + PlenticoreSensorEntityDescription( + module_id="devices:local:battery", + key="P", + name="Battery Power", + native_unit_of_measurement=POWER_WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + formatter="format_round", + ), + PlenticoreSensorEntityDescription( + module_id="devices:local:battery", + key="SoC", + name="Battery SoC", + native_unit_of_measurement=PERCENTAGE, + device_class=SensorDeviceClass.BATTERY, + formatter="format_round", + ), + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:Autarky:Day", + name="Autarky Day", + native_unit_of_measurement=PERCENTAGE, + icon="mdi:chart-donut", + formatter="format_round", + ), + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:Autarky:Month", + name="Autarky Month", + native_unit_of_measurement=PERCENTAGE, + icon="mdi:chart-donut", + formatter="format_round", + ), + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:Autarky:Total", + name="Autarky Total", + native_unit_of_measurement=PERCENTAGE, + icon="mdi:chart-donut", + state_class=SensorStateClass.MEASUREMENT, + formatter="format_round", + ), + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:Autarky:Year", + name="Autarky Year", + native_unit_of_measurement=PERCENTAGE, + icon="mdi:chart-donut", + formatter="format_round", + ), + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:OwnConsumptionRate:Day", + name="Own Consumption Rate Day", + native_unit_of_measurement=PERCENTAGE, + icon="mdi:chart-donut", + formatter="format_round", + ), + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:OwnConsumptionRate:Month", + name="Own Consumption Rate Month", + native_unit_of_measurement=PERCENTAGE, + icon="mdi:chart-donut", + formatter="format_round", + ), + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:OwnConsumptionRate:Total", + name="Own Consumption Rate Total", + native_unit_of_measurement=PERCENTAGE, + icon="mdi:chart-donut", + state_class=SensorStateClass.MEASUREMENT, + formatter="format_round", + ), + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:OwnConsumptionRate:Year", + name="Own Consumption Rate Year", + native_unit_of_measurement=PERCENTAGE, + icon="mdi:chart-donut", + formatter="format_round", + ), + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:EnergyHome:Day", + name="Home Consumption Day", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + formatter="format_energy", + ), + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:EnergyHome:Month", + name="Home Consumption Month", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + formatter="format_energy", + ), + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:EnergyHome:Year", + name="Home Consumption Year", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + formatter="format_energy", + ), + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:EnergyHome:Total", + name="Home Consumption Total", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + formatter="format_energy", + ), + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:EnergyHomeBat:Day", + name="Home Consumption from Battery Day", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + formatter="format_energy", + ), + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:EnergyHomeBat:Month", + name="Home Consumption from Battery Month", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + formatter="format_energy", + ), + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:EnergyHomeBat:Year", + name="Home Consumption from Battery Year", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + formatter="format_energy", + ), + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:EnergyHomeBat:Total", + name="Home Consumption from Battery Total", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + formatter="format_energy", + ), + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:EnergyHomeGrid:Day", + name="Home Consumption from Grid Day", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + formatter="format_energy", + ), + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:EnergyHomeGrid:Month", + name="Home Consumption from Grid Month", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + formatter="format_energy", + ), + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:EnergyHomeGrid:Year", + name="Home Consumption from Grid Year", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + formatter="format_energy", + ), + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:EnergyHomeGrid:Total", + name="Home Consumption from Grid Total", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + formatter="format_energy", + ), + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:EnergyHomePv:Day", + name="Home Consumption from PV Day", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + formatter="format_energy", + ), + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:EnergyHomePv:Month", + name="Home Consumption from PV Month", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + formatter="format_energy", + ), + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:EnergyHomePv:Year", + name="Home Consumption from PV Year", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + formatter="format_energy", + ), + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:EnergyHomePv:Total", + name="Home Consumption from PV Total", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + formatter="format_energy", + ), + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:EnergyPv1:Day", + name="Energy PV1 Day", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + formatter="format_energy", + ), + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:EnergyPv1:Month", + name="Energy PV1 Month", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + formatter="format_energy", + ), + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:EnergyPv1:Year", + name="Energy PV1 Year", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + formatter="format_energy", + ), + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:EnergyPv1:Total", + name="Energy PV1 Total", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + formatter="format_energy", + ), + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:EnergyPv2:Day", + name="Energy PV2 Day", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + formatter="format_energy", + ), + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:EnergyPv2:Month", + name="Energy PV2 Month", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + formatter="format_energy", + ), + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:EnergyPv2:Year", + name="Energy PV2 Year", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + formatter="format_energy", + ), + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:EnergyPv2:Total", + name="Energy PV2 Total", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + formatter="format_energy", + ), + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:EnergyPv3:Day", + name="Energy PV3 Day", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + formatter="format_energy", + ), + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:EnergyPv3:Month", + name="Energy PV3 Month", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + formatter="format_energy", + ), + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:EnergyPv3:Year", + name="Energy PV3 Year", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + formatter="format_energy", + ), + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:EnergyPv3:Total", + name="Energy PV3 Total", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + formatter="format_energy", + ), + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:Yield:Day", + name="Energy Yield Day", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + entity_registry_enabled_default=True, + formatter="format_energy", + ), + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:Yield:Month", + name="Energy Yield Month", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + formatter="format_energy", + ), + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:Yield:Year", + name="Energy Yield Year", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + formatter="format_energy", + ), + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:Yield:Total", + name="Energy Yield Total", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + formatter="format_energy", + ), + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:EnergyChargeGrid:Day", + name="Battery Charge from Grid Day", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + formatter="format_energy", + ), + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:EnergyChargeGrid:Month", + name="Battery Charge from Grid Month", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + formatter="format_energy", + ), + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:EnergyChargeGrid:Year", + name="Battery Charge from Grid Year", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + formatter="format_energy", + ), + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:EnergyChargeGrid:Total", + name="Battery Charge from Grid Total", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + formatter="format_energy", + ), + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:EnergyChargePv:Day", + name="Battery Charge from PV Day", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + formatter="format_energy", + ), + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:EnergyChargePv:Month", + name="Battery Charge from PV Month", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + formatter="format_energy", + ), + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:EnergyChargePv:Year", + name="Battery Charge from PV Year", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + formatter="format_energy", + ), + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:EnergyChargePv:Total", + name="Battery Charge from PV Total", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + formatter="format_energy", + ), + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:EnergyDischargeGrid:Day", + name="Energy Discharge to Grid Day", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + formatter="format_energy", + ), + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:EnergyDischargeGrid:Month", + name="Energy Discharge to Grid Month", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + formatter="format_energy", + ), + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:EnergyDischargeGrid:Year", + name="Energy Discharge to Grid Year", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + formatter="format_energy", + ), + PlenticoreSensorEntityDescription( + module_id="scb:statistic:EnergyFlow", + key="Statistic:EnergyDischargeGrid:Total", + name="Energy Discharge to Grid Total", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + formatter="format_energy", + ), +] + + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: @@ -36,18 +700,9 @@ async def async_setup_entry( timedelta(seconds=10), plenticore, ) - module_id: str - data_id: str - name: str - sensor_data: dict[str, Any] - fmt: str - for ( # type: ignore[assignment] - module_id, - data_id, - name, - sensor_data, - fmt, - ) in SENSOR_PROCESS_DATA: + for description in SENSOR_PROCESS_DATA: + module_id = description.module_id + data_id = description.key if ( module_id not in available_process_data or data_id not in available_process_data[module_id] @@ -60,15 +715,10 @@ async def async_setup_entry( entities.append( PlenticoreDataSensor( process_data_update_coordinator, + description, entry.entry_id, entry.title, - module_id, - data_id, - name, - sensor_data, - PlenticoreDataFormatter.get_method(fmt), plenticore.device_info, - None, ) ) @@ -78,34 +728,31 @@ async def async_setup_entry( class PlenticoreDataSensor(CoordinatorEntity, SensorEntity): """Representation of a Plenticore data Sensor.""" + entity_description: PlenticoreSensorEntityDescription + def __init__( self, - coordinator, + coordinator: ProcessDataUpdateCoordinator, + description: PlenticoreSensorEntityDescription, entry_id: str, platform_name: str, - module_id: str, - data_id: str, - sensor_name: str, - sensor_data: dict[str, Any], - formatter: Callable[[str], Any], device_info: DeviceInfo, - entity_category: EntityCategory | None, - ): + ) -> None: """Create a new Sensor Entity for Plenticore process data.""" super().__init__(coordinator) + self.entity_description = description self.entry_id = entry_id self.platform_name = platform_name - self.module_id = module_id - self.data_id = data_id + self.module_id = description.module_id + self.data_id = description.key - self._sensor_name = sensor_name - self._sensor_data = sensor_data - self._formatter = formatter + self._sensor_name = description.name + self._formatter: Callable[[str], Any] = PlenticoreDataFormatter.get_method( + description.formatter + ) self._device_info = device_info - self._attr_entity_category = entity_category - @property def available(self) -> bool: """Return if entity is available.""" @@ -141,31 +788,6 @@ class PlenticoreDataSensor(CoordinatorEntity, SensorEntity): """Return the name of this Sensor Entity.""" return f"{self.platform_name} {self._sensor_name}" - @property - def native_unit_of_measurement(self) -> str | None: - """Return the unit of this Sensor Entity or None.""" - return self._sensor_data.get(ATTR_UNIT_OF_MEASUREMENT) - - @property - def icon(self) -> str | None: - """Return the icon name of this Sensor Entity or None.""" - return self._sensor_data.get(ATTR_ICON) - - @property - def device_class(self) -> str | None: - """Return the class of this device, from component DEVICE_CLASSES.""" - return self._sensor_data.get(ATTR_DEVICE_CLASS) - - @property - def state_class(self) -> str | None: - """Return the class of the state of this device, from component STATE_CLASSES.""" - return self._sensor_data.get(ATTR_STATE_CLASS) - - @property - def entity_registry_enabled_default(self) -> bool: - """Return if the entity should be enabled when first added to the entity registry.""" - return self._sensor_data.get(ATTR_ENABLED_DEFAULT, False) - @property def native_value(self) -> Any | None: """Return the state of the sensor.""" @@ -175,4 +797,4 @@ class PlenticoreDataSensor(CoordinatorEntity, SensorEntity): raw_value = self.coordinator.data[self.module_id][self.data_id] - return self._formatter(raw_value) if self._formatter else raw_value + return self._formatter(raw_value) diff --git a/homeassistant/components/kostal_plenticore/switch.py b/homeassistant/components/kostal_plenticore/switch.py index 01ef16069ab..90ac26ef947 100644 --- a/homeassistant/components/kostal_plenticore/switch.py +++ b/homeassistant/components/kostal_plenticore/switch.py @@ -1,23 +1,57 @@ """Platform for Kostal Plenticore switches.""" from __future__ import annotations -from abc import ABC +from dataclasses import dataclass from datetime import timedelta import logging +from typing import Any -from homeassistant.components.switch import SwitchEntity +from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo, EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import DOMAIN, SWITCH_SETTINGS_DATA +from .const import DOMAIN from .helper import SettingDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) +@dataclass +class PlenticoreRequiredKeysMixin: + """A class that describes required properties for plenticore switch entities.""" + + module_id: str + is_on: str + on_value: str + on_label: str + off_value: str + off_label: str + + +@dataclass +class PlenticoreSwitchEntityDescription( + SwitchEntityDescription, PlenticoreRequiredKeysMixin +): + """A class that describes plenticore switch entities.""" + + +SWITCH_SETTINGS_DATA = [ + PlenticoreSwitchEntityDescription( + module_id="devices:local", + key="Battery:Strategy", + name="Battery Strategy", + is_on="1", + on_value="1", + on_label="Automatic", + off_value="2", + off_label="Automatic economical", + ), +] + + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: @@ -34,75 +68,65 @@ async def async_setup_entry( timedelta(seconds=30), plenticore, ) - for switch in SWITCH_SETTINGS_DATA: - if switch.module_id not in available_settings_data or switch.data_id not in ( - setting.id for setting in available_settings_data[switch.module_id] + for description in SWITCH_SETTINGS_DATA: + if ( + description.module_id not in available_settings_data + or description.key + not in ( + setting.id for setting in available_settings_data[description.module_id] + ) ): _LOGGER.debug( "Skipping non existing setting data %s/%s", - switch.module_id, - switch.data_id, + description.module_id, + description.key, ) continue entities.append( PlenticoreDataSwitch( settings_data_update_coordinator, + description, entry.entry_id, entry.title, - switch.module_id, - switch.data_id, - switch.name, - switch.is_on, - switch.on_value, - switch.on_label, - switch.off_value, - switch.off_label, plenticore.device_info, - f"{entry.title} {switch.name}", - f"{entry.entry_id}_{switch.module_id}_{switch.data_id}", ) ) async_add_entities(entities) -class PlenticoreDataSwitch(CoordinatorEntity, SwitchEntity, ABC): +class PlenticoreDataSwitch( + CoordinatorEntity[SettingDataUpdateCoordinator], SwitchEntity +): """Representation of a Plenticore Switch.""" _attr_entity_category = EntityCategory.CONFIG + entity_description: PlenticoreSwitchEntityDescription def __init__( self, - coordinator, + coordinator: SettingDataUpdateCoordinator, + description: PlenticoreSwitchEntityDescription, entry_id: str, platform_name: str, - module_id: str, - data_id: str, - name: str, - is_on: str, - on_value: str, - on_label: str, - off_value: str, - off_label: str, device_info: DeviceInfo, - attr_name: str, - attr_unique_id: str, - ): + ) -> None: """Create a new Switch Entity for Plenticore process data.""" super().__init__(coordinator) + self.entity_description = description self.entry_id = entry_id self.platform_name = platform_name - self.module_id = module_id - self.data_id = data_id - self._name = name - self._is_on = is_on - self._attr_name = attr_name - self.on_value = on_value - self.on_label = on_label - self.off_value = off_value - self.off_label = off_label - self._attr_unique_id = attr_unique_id + self.module_id = description.module_id + self.data_id = description.key + self._name = description.name + self._is_on = description.is_on + self._attr_name = f"{platform_name} {description.name}" + self.on_value = description.on_value + self.on_label = description.on_label + self.off_value = description.off_value + self.off_label = description.off_label + self._attr_unique_id = f"{entry_id}_{description.module_id}_{description.key}" self._device_info = device_info @@ -126,7 +150,7 @@ class PlenticoreDataSwitch(CoordinatorEntity, SwitchEntity, ABC): self.coordinator.stop_fetch_data(self.module_id, self.data_id) await super().async_will_remove_from_hass() - async def async_turn_on(self, **kwargs) -> None: + async def async_turn_on(self, **kwargs: Any) -> None: """Turn device on.""" if await self.coordinator.async_write_data( self.module_id, {self.data_id: self.on_value} @@ -134,7 +158,7 @@ class PlenticoreDataSwitch(CoordinatorEntity, SwitchEntity, ABC): self.coordinator.name = f"{self.platform_name} {self._name} {self.on_label}" await self.coordinator.async_request_refresh() - async def async_turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs: Any) -> None: """Turn device off.""" if await self.coordinator.async_write_data( self.module_id, {self.data_id: self.off_value} diff --git a/homeassistant/components/kraken/const.py b/homeassistant/components/kraken/const.py index ae626349cb0..816fb35fadb 100644 --- a/homeassistant/components/kraken/const.py +++ b/homeassistant/components/kraken/const.py @@ -50,77 +50,93 @@ class KrakenSensorEntityDescription(SensorEntityDescription, KrakenRequiredKeysM SENSOR_TYPES: tuple[KrakenSensorEntityDescription, ...] = ( KrakenSensorEntityDescription( key="ask", + name="Ask", value_fn=lambda x, y: x.data[y]["ask"][0], ), KrakenSensorEntityDescription( key="ask_volume", + name="Ask Volume", value_fn=lambda x, y: x.data[y]["ask"][1], entity_registry_enabled_default=False, ), KrakenSensorEntityDescription( key="bid", + name="Bid", value_fn=lambda x, y: x.data[y]["bid"][0], ), KrakenSensorEntityDescription( key="bid_volume", + name="Bid Volume", value_fn=lambda x, y: x.data[y]["bid"][1], entity_registry_enabled_default=False, ), KrakenSensorEntityDescription( key="volume_today", + name="Volume Today", value_fn=lambda x, y: x.data[y]["volume"][0], entity_registry_enabled_default=False, ), KrakenSensorEntityDescription( key="volume_last_24h", + name="Volume last 24h", value_fn=lambda x, y: x.data[y]["volume"][1], entity_registry_enabled_default=False, ), KrakenSensorEntityDescription( key="volume_weighted_average_today", + name="Volume weighted average today", value_fn=lambda x, y: x.data[y]["volume_weighted_average"][0], entity_registry_enabled_default=False, ), KrakenSensorEntityDescription( key="volume_weighted_average_last_24h", + name="Volume weighted average last 24h", value_fn=lambda x, y: x.data[y]["volume_weighted_average"][1], entity_registry_enabled_default=False, ), KrakenSensorEntityDescription( key="number_of_trades_today", + name="Number of trades today", value_fn=lambda x, y: x.data[y]["number_of_trades"][0], entity_registry_enabled_default=False, ), KrakenSensorEntityDescription( key="number_of_trades_last_24h", + name="Number of trades last 24h", value_fn=lambda x, y: x.data[y]["number_of_trades"][1], entity_registry_enabled_default=False, ), KrakenSensorEntityDescription( key="last_trade_closed", + name="Last trade closed", value_fn=lambda x, y: x.data[y]["last_trade_closed"][0], entity_registry_enabled_default=False, ), KrakenSensorEntityDescription( key="low_today", + name="Low today", value_fn=lambda x, y: x.data[y]["low"][0], ), KrakenSensorEntityDescription( key="low_last_24h", + name="Low last 24h", value_fn=lambda x, y: x.data[y]["low"][1], entity_registry_enabled_default=False, ), KrakenSensorEntityDescription( key="high_today", + name="High today", value_fn=lambda x, y: x.data[y]["high"][0], ), KrakenSensorEntityDescription( key="high_last_24h", + name="High last 24h", value_fn=lambda x, y: x.data[y]["high"][1], entity_registry_enabled_default=False, ), KrakenSensorEntityDescription( key="opening_price_today", + name="Opening price today", value_fn=lambda x, y: x.data[y]["opening_price"], entity_registry_enabled_default=False, ), diff --git a/homeassistant/components/kraken/sensor.py b/homeassistant/components/kraken/sensor.py index f98a48d8dc3..d37ecfea889 100644 --- a/homeassistant/components/kraken/sensor.py +++ b/homeassistant/components/kraken/sensor.py @@ -109,19 +109,17 @@ class KrakenSensor( self.tracked_asset_pair_wsname = kraken_data.tradable_asset_pairs[ tracked_asset_pair ] - source_asset = tracked_asset_pair.split("/")[0] self._target_asset = tracked_asset_pair.split("/")[1] if "number_of" not in description.key: self._attr_native_unit_of_measurement = self._target_asset - self._device_name = f"{source_asset} {self._target_asset}" - self._attr_name = "_".join( + self._device_name = create_device_name(tracked_asset_pair) + self._attr_unique_id = "_".join( [ tracked_asset_pair.split("/")[0], tracked_asset_pair.split("/")[1], description.key, ] - ) - self._attr_unique_id = self._attr_name.lower() + ).lower() self._received_data_at_least_once = False self._available = True self._attr_state_class = SensorStateClass.MEASUREMENT @@ -129,10 +127,11 @@ class KrakenSensor( self._attr_device_info = DeviceInfo( configuration_url="https://www.kraken.com/", entry_type=device_registry.DeviceEntryType.SERVICE, - identifiers={(DOMAIN, f"{source_asset}_{self._target_asset}")}, + identifiers={(DOMAIN, "_".join(self._device_name.split(" ")))}, manufacturer="Kraken.com", name=self._device_name, ) + self._attr_has_entity_name = True async def async_added_to_hass(self) -> None: """Handle entity which will be added.""" diff --git a/homeassistant/components/kraken/translations/el.json b/homeassistant/components/kraken/translations/el.json index 252d1a5ebd2..aa3c6780ac9 100644 --- a/homeassistant/components/kraken/translations/el.json +++ b/homeassistant/components/kraken/translations/el.json @@ -5,6 +5,10 @@ }, "step": { "user": { + "data": { + "one": "\u03ba\u03b5\u03bd\u03cc", + "other": "\u03ba\u03b5\u03bd\u03cc" + }, "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03be\u03b5\u03ba\u03b9\u03bd\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7;" } } diff --git a/homeassistant/components/kraken/translations/tr.json b/homeassistant/components/kraken/translations/tr.json index fa3decd322e..0d54a302a50 100644 --- a/homeassistant/components/kraken/translations/tr.json +++ b/homeassistant/components/kraken/translations/tr.json @@ -11,7 +11,7 @@ "user": { "data": { "one": "Bo\u015f", - "other": "" + "other": "Bo\u015f" }, "description": "Kuruluma ba\u015flamak ister misiniz?" } diff --git a/homeassistant/components/kulersky/light.py b/homeassistant/components/kulersky/light.py index c1cc68c9035..c6763e6d9f6 100644 --- a/homeassistant/components/kulersky/light.py +++ b/homeassistant/components/kulersky/light.py @@ -139,7 +139,7 @@ class KulerskyLight(LightEntity): """Instruct the light to turn off.""" await self._light.set_color(0, 0, 0, 0) - async def async_update(self): + async def async_update(self) -> None: """Fetch new state data for this light.""" try: if not self._available: @@ -156,8 +156,13 @@ class KulerskyLight(LightEntity): self._available = True brightness = max(rgbw) if not brightness: - rgbw_normalized = [0, 0, 0, 0] + self._attr_rgbw_color = (0, 0, 0, 0) else: rgbw_normalized = [round(x * 255 / brightness) for x in rgbw] + self._attr_rgbw_color = ( + rgbw_normalized[0], + rgbw_normalized[1], + rgbw_normalized[2], + rgbw_normalized[3], + ) self._attr_brightness = brightness - self._attr_rgbw_color = tuple(rgbw_normalized) diff --git a/homeassistant/components/lacrosse_view/sensor.py b/homeassistant/components/lacrosse_view/sensor.py index 46c4671a109..684ac884345 100644 --- a/homeassistant/components/lacrosse_view/sensor.py +++ b/homeassistant/components/lacrosse_view/sensor.py @@ -70,17 +70,18 @@ SENSOR_DESCRIPTIONS = { "HeatIndex": LaCrosseSensorEntityDescription( key="HeatIndex", device_class=SensorDeviceClass.TEMPERATURE, - name="Heat Index", + name="Heat index", state_class=SensorStateClass.MEASUREMENT, value_fn=get_value, native_unit_of_measurement=TEMP_FAHRENHEIT, ), "WindSpeed": LaCrosseSensorEntityDescription( key="WindSpeed", - name="Wind Speed", + name="Wind speed", state_class=SensorStateClass.MEASUREMENT, value_fn=get_value, native_unit_of_measurement=SPEED_KILOMETERS_PER_HOUR, + device_class=SensorDeviceClass.SPEED, ), "Rain": LaCrosseSensorEntityDescription( key="Rain", @@ -104,7 +105,7 @@ async def async_setup_entry( sensors: list[Sensor] = coordinator.data sensor_list = [] - for sensor in sensors: + for i, sensor in enumerate(sensors): for field in sensor.sensor_field_names: description = SENSOR_DESCRIPTIONS.get(field) if description is None: @@ -124,6 +125,7 @@ async def async_setup_entry( coordinator=coordinator, description=description, sensor=sensor, + index=i, ) ) @@ -136,31 +138,32 @@ class LaCrosseViewSensor( """LaCrosse View sensor.""" entity_description: LaCrosseSensorEntityDescription + _attr_has_entity_name: bool = True def __init__( self, description: LaCrosseSensorEntityDescription, coordinator: DataUpdateCoordinator[list[Sensor]], sensor: Sensor, + index: int, ) -> None: """Initialize.""" super().__init__(coordinator) self.entity_description = description self._attr_unique_id = f"{sensor.sensor_id}-{description.key}" - self._attr_name = f"{sensor.location.name} {description.name}" self._attr_device_info = { "identifiers": {(DOMAIN, sensor.sensor_id)}, - "name": sensor.name.split(" ")[0], + "name": sensor.name, "manufacturer": "LaCrosse Technology", "model": sensor.model, "via_device": (DOMAIN, sensor.location.id), } - self._sensor = sensor + self.index = index @property def native_value(self) -> float | str: """Return the sensor value.""" return self.entity_description.value_fn( - self._sensor, self.entity_description.key + self.coordinator.data[self.index], self.entity_description.key ) diff --git a/homeassistant/components/lacrosse_view/translations/bg.json b/homeassistant/components/lacrosse_view/translations/bg.json new file mode 100644 index 00000000000..652dea38fcc --- /dev/null +++ b/homeassistant/components/lacrosse_view/translations/bg.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" + }, + "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": "\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", + "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/lacrosse_view/translations/cs.json b/homeassistant/components/lacrosse_view/translations/cs.json new file mode 100644 index 00000000000..0f5d2655f6c --- /dev/null +++ b/homeassistant/components/lacrosse_view/translations/cs.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" + }, + "error": { + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lacrosse_view/translations/es.json b/homeassistant/components/lacrosse_view/translations/es.json index 9b02b2bbd4f..63cf0081be9 100644 --- a/homeassistant/components/lacrosse_view/translations/es.json +++ b/homeassistant/components/lacrosse_view/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "error": { "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", diff --git a/homeassistant/components/lacrosse_view/translations/sv.json b/homeassistant/components/lacrosse_view/translations/sv.json index 241263b2c53..cea713d675c 100644 --- a/homeassistant/components/lacrosse_view/translations/sv.json +++ b/homeassistant/components/lacrosse_view/translations/sv.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Enheten \u00e4r redan konfigurerad" + "already_configured": "Enheten \u00e4r redan konfigurerad", + "reauth_successful": "\u00c5terautentisering lyckades" }, "error": { "invalid_auth": "Ogiltig autentisering", diff --git a/homeassistant/components/lametric/config_flow.py b/homeassistant/components/lametric/config_flow.py index 59797e3caf1..a317d835413 100644 --- a/homeassistant/components/lametric/config_flow.py +++ b/homeassistant/components/lametric/config_flow.py @@ -21,6 +21,7 @@ from demetriek import ( import voluptuous as vol from yarl import URL +from homeassistant.components.dhcp import DhcpServiceInfo from homeassistant.components.ssdp import ( ATTR_UPNP_FRIENDLY_NAME, ATTR_UPNP_SERIAL, @@ -30,6 +31,7 @@ from homeassistant.const import CONF_API_KEY, CONF_DEVICE, CONF_HOST, CONF_MAC from homeassistant.data_entry_flow import AbortFlow, FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.config_entry_oauth2_flow import AbstractOAuth2FlowHandler +from homeassistant.helpers.device_registry import format_mac from homeassistant.helpers.selector import ( SelectOptionDict, SelectSelector, @@ -247,6 +249,22 @@ class LaMetricFlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN): }, ) + async def async_step_dhcp(self, discovery_info: DhcpServiceInfo) -> FlowResult: + """Handle dhcp discovery to update existing entries.""" + mac = format_mac(discovery_info.macaddress) + for entry in self._async_current_entries(): + if format_mac(entry.data[CONF_MAC]) == mac: + self.hass.config_entries.async_update_entry( + entry, + data=entry.data | {CONF_HOST: discovery_info.ip}, + ) + self.hass.async_create_task( + self.hass.config_entries.async_reload(entry.entry_id) + ) + return self.async_abort(reason="already_configured") + + return self.async_abort(reason="unknown") + # Replace OAuth create entry with a fetch devices step # LaMetric only use OAuth to get device information, but doesn't # use it later on. diff --git a/homeassistant/components/lametric/manifest.json b/homeassistant/components/lametric/manifest.json index 9fb39f9fb47..cddf28e5487 100644 --- a/homeassistant/components/lametric/manifest.json +++ b/homeassistant/components/lametric/manifest.json @@ -12,5 +12,6 @@ { "deviceType": "urn:schemas-upnp-org:device:LaMetric:1" } - ] + ], + "dhcp": [{ "registered_devices": true }] } diff --git a/homeassistant/components/lametric/strings.json b/homeassistant/components/lametric/strings.json index 53271a8d0d8..433f70df18d 100644 --- a/homeassistant/components/lametric/strings.json +++ b/homeassistant/components/lametric/strings.json @@ -38,7 +38,8 @@ "link_local_address": "Link local addresses are not supported", "missing_configuration": "The LaMetric integration is not configured. Please follow the documentation.", "no_devices": "The authorized user has no LaMetric devices", - "no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]" + "no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]", + "unknown": "[%key:common::config_flow::error::unknown%]" } }, "issues": { diff --git a/homeassistant/components/lametric/translations/bg.json b/homeassistant/components/lametric/translations/bg.json new file mode 100644 index 00000000000..28104788cbd --- /dev/null +++ b/homeassistant/components/lametric/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", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "choice_enter_manual_or_fetch_cloud": { + "menu_options": { + "manual_entry": "\u0412\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u0440\u044a\u0447\u043d\u043e", + "pick_implementation": "\u0418\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u0430\u043d\u0435 \u043e\u0442 LaMetric.com (\u043f\u0440\u0435\u043f\u043e\u0440\u044a\u0447\u0438\u0442\u0435\u043b\u043d\u043e)" + } + }, + "manual_entry": { + "data": { + "api_key": "API \u043a\u043b\u044e\u0447", + "host": "\u0425\u043e\u0441\u0442" + } + }, + "pick_implementation": { + "title": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u043c\u0435\u0442\u043e\u0434 \u0437\u0430 \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435" + }, + "user_cloud_select_device": { + "data": { + "device": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e LaMetric, \u043a\u043e\u0435\u0442\u043e \u0438\u0441\u043a\u0430\u0442\u0435 \u0434\u0430 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u0435" + } + } + } + }, + "issues": { + "manual_migration": { + "title": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u0430 \u0435 \u0440\u044a\u0447\u043d\u0430 \u043c\u0438\u0433\u0440\u0430\u0446\u0438\u044f \u0437\u0430 LaMetric" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lametric/translations/ca.json b/homeassistant/components/lametric/translations/ca.json index 4c8f002ce68..0c7a30099e5 100644 --- a/homeassistant/components/lametric/translations/ca.json +++ b/homeassistant/components/lametric/translations/ca.json @@ -4,9 +4,11 @@ "already_configured": "El dispositiu ja est\u00e0 configurat", "authorize_url_timeout": "Temps d'espera esgotat durant la generaci\u00f3 de l'URL d'autoritzaci\u00f3.", "invalid_discovery_info": "S'ha rebut informaci\u00f3 de descobriment no v\u00e0lida", + "link_local_address": "L'enlla\u00e7 amb adreces locals no est\u00e0 perm\u00e8s", "missing_configuration": "La integraci\u00f3 LaMetric no est\u00e0 configurada. Consulta la documentaci\u00f3.", "no_devices": "L'usuari autoritzat no t\u00e9 cap dispositiu LaMetric", - "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})" + "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})", + "unknown": "Error inesperat" }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3", @@ -14,6 +16,7 @@ }, "step": { "choice_enter_manual_or_fetch_cloud": { + "description": "Els dispositius LaMetric es poden configurar a Home Assistant de dues maneres diferents. \n\nPots introduir tota la informaci\u00f3 del dispositiu i els 'tokens' API, o b\u00e9, Home Assistant els pot importar des del teu compte de LaMetric.com.", "menu_options": { "manual_entry": "Introdueix manualment", "pick_implementation": "Importa des de LaMetric.com (recomanat)" @@ -38,5 +41,11 @@ } } } + }, + "issues": { + "manual_migration": { + "description": "La integraci\u00f3 de LaMetric s'ha modernitzat: ara es configura a trav\u00e9s de la interf\u00edcie d'usuari (IU) i les comunicacions s\u00f3n locals. \n\nMalauradament, no hi ha cap possibilitat de migraci\u00f3 autom\u00e0tica i, per tant, cal que tornis a configurar el teu dispositiu LaMetric amb Home Assistant. Consulta la documentaci\u00f3 de la integraci\u00f3 LaMetric de Home Assistant per fer-ho. \n\nElimina l'antiga configuraci\u00f3 YAML de LaMetric del fitxer configuration.yaml i reinicia Home Assistant per solucionar aquest problema.", + "title": "\u00c9s necess\u00e0ria una migraci\u00f3 manual per a LaMetric" + } } } \ No newline at end of file diff --git a/homeassistant/components/lametric/translations/cs.json b/homeassistant/components/lametric/translations/cs.json new file mode 100644 index 00000000000..59280c6c2bc --- /dev/null +++ b/homeassistant/components/lametric/translations/cs.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "authorize_url_timeout": "\u010casov\u00fd limit autoriza\u010dn\u00edho URL vypr\u0161el", + "no_url_available": "Nen\u00ed k dispozici \u017e\u00e1dn\u00e1 adresa URL. Informace o t\u00e9to chyb\u011b naleznete [v sekci n\u00e1pov\u011bdy]({docs_url})" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "manual_entry": { + "data": { + "api_key": "Kl\u00ed\u010d API", + "host": "Hostitel" + } + }, + "pick_implementation": { + "title": "Vyberte metodu ov\u011b\u0159en\u00ed" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lametric/translations/de.json b/homeassistant/components/lametric/translations/de.json index dab436c4b2d..8f347963182 100644 --- a/homeassistant/components/lametric/translations/de.json +++ b/homeassistant/components/lametric/translations/de.json @@ -7,7 +7,8 @@ "link_local_address": "Lokale Linkadressen werden nicht unterst\u00fctzt", "missing_configuration": "Die LaMetric-Integration ist nicht konfiguriert. Bitte folge der Dokumentation.", "no_devices": "Der autorisierte Benutzer hat keine LaMetric Ger\u00e4te", - "no_url_available": "Keine URL verf\u00fcgbar. Informationen zu diesem Fehler findest du [im Hilfebereich]({docs_url})." + "no_url_available": "Keine URL verf\u00fcgbar. Informationen zu diesem Fehler findest du [im Hilfebereich]({docs_url}).", + "unknown": "Unerwarteter Fehler" }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", diff --git a/homeassistant/components/lametric/translations/el.json b/homeassistant/components/lametric/translations/el.json index 964bfc4b212..8733a19e690 100644 --- a/homeassistant/components/lametric/translations/el.json +++ b/homeassistant/components/lametric/translations/el.json @@ -7,7 +7,8 @@ "link_local_address": "\u039f\u03b9 \u03c4\u03bf\u03c0\u03b9\u03ba\u03ad\u03c2 \u03b4\u03b9\u03b5\u03c5\u03b8\u03cd\u03bd\u03c3\u03b5\u03b9\u03c2 \u03c3\u03c5\u03bd\u03b4\u03ad\u03c3\u03bc\u03bf\u03c5 \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03bf\u03bd\u03c4\u03b1\u03b9", "missing_configuration": "\u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 LaMetric \u03b4\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af. \u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03bf\u03cd\u03bc\u03b5 \u03b1\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7.", "no_devices": "\u039f \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03bf\u03c4\u03b7\u03bc\u03ad\u03bd\u03bf\u03c2 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7\u03c2 \u03b4\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 LaMetric", - "no_url_available": "\u0394\u03b5\u03bd \u03c5\u03c0\u03ac\u03c1\u03c7\u03b5\u03b9 \u03b4\u03b9\u03b1\u03b8\u03ad\u03c3\u03b9\u03bc\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL. \u0393\u03b9\u03b1 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 \u03c3\u03c7\u03b5\u03c4\u03b9\u03ba\u03ac \u03bc\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1, [\u03b5\u03bb\u03ad\u03b3\u03be\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b5\u03bd\u03cc\u03c4\u03b7\u03c4\u03b1 \u03b2\u03bf\u03ae\u03b8\u03b5\u03b9\u03b1\u03c2] ( {docs_url} )" + "no_url_available": "\u0394\u03b5\u03bd \u03c5\u03c0\u03ac\u03c1\u03c7\u03b5\u03b9 \u03b4\u03b9\u03b1\u03b8\u03ad\u03c3\u03b9\u03bc\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL. \u0393\u03b9\u03b1 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 \u03c3\u03c7\u03b5\u03c4\u03b9\u03ba\u03ac \u03bc\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1, [\u03b5\u03bb\u03ad\u03b3\u03be\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b5\u03bd\u03cc\u03c4\u03b7\u03c4\u03b1 \u03b2\u03bf\u03ae\u03b8\u03b5\u03b9\u03b1\u03c2] ( {docs_url} )", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, "error": { "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", diff --git a/homeassistant/components/lametric/translations/en.json b/homeassistant/components/lametric/translations/en.json index c02b7d6d05f..52e483ec1f0 100644 --- a/homeassistant/components/lametric/translations/en.json +++ b/homeassistant/components/lametric/translations/en.json @@ -7,7 +7,8 @@ "link_local_address": "Link local addresses are not supported", "missing_configuration": "The LaMetric integration is not configured. Please follow the documentation.", "no_devices": "The authorized user has no LaMetric devices", - "no_url_available": "No URL available. For information about this error, [check the help section]({docs_url})" + "no_url_available": "No URL available. For information about this error, [check the help section]({docs_url})", + "unknown": "Unexpected error" }, "error": { "cannot_connect": "Failed to connect", diff --git a/homeassistant/components/lametric/translations/es.json b/homeassistant/components/lametric/translations/es.json index e7cbe914a2e..7cc6fc36cf3 100644 --- a/homeassistant/components/lametric/translations/es.json +++ b/homeassistant/components/lametric/translations/es.json @@ -7,7 +7,8 @@ "link_local_address": "Las direcciones de enlace local no son compatibles", "missing_configuration": "La integraci\u00f3n de LaMetric no est\u00e1 configurada. Por favor, sigue la documentaci\u00f3n.", "no_devices": "El usuario autorizado no tiene dispositivos LaMetric", - "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [revisa la secci\u00f3n de ayuda]({docs_url})" + "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [revisa la secci\u00f3n de ayuda]({docs_url})", + "unknown": "Error inesperado" }, "error": { "cannot_connect": "No se pudo conectar", diff --git a/homeassistant/components/lametric/translations/et.json b/homeassistant/components/lametric/translations/et.json index 62340afca03..f2ec397481d 100644 --- a/homeassistant/components/lametric/translations/et.json +++ b/homeassistant/components/lametric/translations/et.json @@ -7,7 +7,8 @@ "link_local_address": "Kohtv\u00f5rgu linke ei toetata", "missing_configuration": "LaMetricu integratsioon pole konfigureeritud. Palun j\u00e4rgige dokumentatsiooni.", "no_devices": "Volitatud kasutajal pole LaMetricu seadmeid", - "no_url_available": "URL pole saadaval. Teavet selle veateate kohta saab [check the help section]({docs_url})" + "no_url_available": "URL pole saadaval. Teavet selle veateate kohta saab [check the help section]({docs_url})", + "unknown": "Ootamatu t\u00f5rge" }, "error": { "cannot_connect": "\u00dchendamine nurjus", diff --git a/homeassistant/components/lametric/translations/fr.json b/homeassistant/components/lametric/translations/fr.json index 5bd4a5899f6..fd0125f68c9 100644 --- a/homeassistant/components/lametric/translations/fr.json +++ b/homeassistant/components/lametric/translations/fr.json @@ -7,7 +7,8 @@ "link_local_address": "Les adresses de liaison locale ne sont pas prises en charge", "missing_configuration": "L'int\u00e9gration LaMetric n'est pas configur\u00e9e\u00a0; veuillez suivre la documentation.", "no_devices": "L'utilisateur autoris\u00e9 ne poss\u00e8de aucun appareil LaMetric", - "no_url_available": "Aucune URL disponible. Pour plus d'informations sur cette erreur, [consultez la section d'aide]({docs_url})" + "no_url_available": "Aucune URL disponible. Pour plus d'informations sur cette erreur, [consultez la section d'aide]({docs_url})", + "unknown": "Erreur inattendue" }, "error": { "cannot_connect": "\u00c9chec de connexion", diff --git a/homeassistant/components/lametric/translations/he.json b/homeassistant/components/lametric/translations/he.json new file mode 100644 index 00000000000..53f74430ae1 --- /dev/null +++ b/homeassistant/components/lametric/translations/he.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "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/lametric/translations/hu.json b/homeassistant/components/lametric/translations/hu.json index c27848279e7..0e326d4b4e8 100644 --- a/homeassistant/components/lametric/translations/hu.json +++ b/homeassistant/components/lametric/translations/hu.json @@ -7,7 +7,8 @@ "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.", "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." + "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.", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", diff --git a/homeassistant/components/lametric/translations/id.json b/homeassistant/components/lametric/translations/id.json index 69f776e636c..e668efa0403 100644 --- a/homeassistant/components/lametric/translations/id.json +++ b/homeassistant/components/lametric/translations/id.json @@ -7,7 +7,8 @@ "link_local_address": "Tautan alamat lokal tidak didukung", "missing_configuration": "Integrasi LaMetric tidak dikonfigurasi. Silakan ikuti dokumentasi.", "no_devices": "Pengguna yang diotorisasi tidak memiliki perangkat LaMetric", - "no_url_available": "Tidak ada URL yang tersedia. Untuk informasi tentang kesalahan ini, [lihat bagian bantuan]({docs_url})" + "no_url_available": "Tidak ada URL yang tersedia. Untuk informasi tentang kesalahan ini, [lihat bagian bantuan]({docs_url})", + "unknown": "Kesalahan yang tidak diharapkan" }, "error": { "cannot_connect": "Gagal terhubung", diff --git a/homeassistant/components/lametric/translations/it.json b/homeassistant/components/lametric/translations/it.json index e41d40d7605..c3496022701 100644 --- a/homeassistant/components/lametric/translations/it.json +++ b/homeassistant/components/lametric/translations/it.json @@ -7,7 +7,8 @@ "link_local_address": "Gli indirizzi locali di collegamento non sono supportati", "missing_configuration": "L'integrazione LaMetric non \u00e8 configurata. Segui la documentazione.", "no_devices": "L'utente autorizzato non dispone di dispositivi LaMetric", - "no_url_available": "Nessun URL disponibile. Per informazioni su questo errore, [controlla la sezione della guida]({docs_url})" + "no_url_available": "Nessun URL disponibile. Per informazioni su questo errore, [controlla la sezione della guida]({docs_url})", + "unknown": "Errore imprevisto" }, "error": { "cannot_connect": "Impossibile connettersi", diff --git a/homeassistant/components/lametric/translations/ja.json b/homeassistant/components/lametric/translations/ja.json index 4f6768ca80b..c3f0da2866c 100644 --- a/homeassistant/components/lametric/translations/ja.json +++ b/homeassistant/components/lametric/translations/ja.json @@ -7,7 +7,8 @@ "link_local_address": "\u30ed\u30fc\u30ab\u30eb\u30a2\u30c9\u30ec\u30b9\u306e\u30ea\u30f3\u30af\u306b\u306f\u5bfe\u5fdc\u3057\u3066\u3044\u307e\u305b\u3093", "missing_configuration": "LaMetric\u306e\u7d71\u5408\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_devices": "\u8a31\u53ef\u3055\u308c\u305f\u30e6\u30fc\u30b6\u30fc\u306f\u3001LaMetric\u30c7\u30d0\u30a4\u30b9\u3092\u6301\u3063\u3066\u3044\u307e\u305b\u3093", - "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})" + "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})", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", @@ -43,6 +44,7 @@ }, "issues": { "manual_migration": { + "description": "LaMetric \u7d71\u5408\u306f\u6700\u65b0\u5316\u3055\u308c\u307e\u3057\u305f\u3002\u30e6\u30fc\u30b6\u30fc \u30a4\u30f3\u30bf\u30fc\u30d5\u30a7\u30a4\u30b9\u3092\u4ecb\u3057\u3066\u69cb\u6210\u304a\u3088\u3073\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3055\u308c\u3001\u901a\u4fe1\u306f\u30ed\u30fc\u30ab\u30eb\u306b\u306a\u308a\u307e\u3057\u305f\u3002 \n\n\u6b8b\u5ff5\u306a\u304c\u3089\u3001\u53ef\u80fd\u306a\u81ea\u52d5\u79fb\u884c\u30d1\u30b9\u304c\u306a\u3044\u305f\u3081\u3001Home Assistant \u3067 LaMetric \u3092\u518d\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u65b9\u6cd5\u306b\u3064\u3044\u3066\u306f\u3001Home Assistant LaMetric \u7d71\u5408\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002 \n\n\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001\u53e4\u3044 LaMetric YAML \u69cb\u6210\u3092 configuration.yaml \u30d5\u30a1\u30a4\u30eb\u304b\u3089\u524a\u9664\u3057\u3001Home Assistant \u3092\u518d\u8d77\u52d5\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "title": "LaMetric\u306b\u5fc5\u8981\u306a\u624b\u52d5\u3067\u306e\u79fb\u884c" } } diff --git a/homeassistant/components/lametric/translations/nl.json b/homeassistant/components/lametric/translations/nl.json index 6cbebff75e3..f88338f4410 100644 --- a/homeassistant/components/lametric/translations/nl.json +++ b/homeassistant/components/lametric/translations/nl.json @@ -3,13 +3,19 @@ "abort": { "already_configured": "Apparaat is al geconfigureerd", "authorize_url_timeout": "Time-out bij het genereren van autorisatie-URL.", - "no_url_available": "Geen URL beschikbaar. Voor informatie over deze fout, [raadpleeg de documentatie]({docs_url})" + "no_url_available": "Geen URL beschikbaar. Voor informatie over deze fout, [raadpleeg de documentatie]({docs_url})", + "unknown": "Onverwachte fout" }, "error": { "cannot_connect": "Kan geen verbinding maken", "unknown": "Onverwachte fout" }, "step": { + "choice_enter_manual_or_fetch_cloud": { + "menu_options": { + "manual_entry": "Handmatig invoeren" + } + }, "manual_entry": { "data": { "api_key": "API-sleutel", diff --git a/homeassistant/components/lametric/translations/no.json b/homeassistant/components/lametric/translations/no.json index 79d3591aec1..4984e190241 100644 --- a/homeassistant/components/lametric/translations/no.json +++ b/homeassistant/components/lametric/translations/no.json @@ -7,7 +7,8 @@ "link_local_address": "Lokale koblingsadresser st\u00f8ttes ikke", "missing_configuration": "LaMetric-integrasjonen er ikke konfigurert. Vennligst f\u00f8lg dokumentasjonen.", "no_devices": "Den autoriserte brukeren har ingen LaMetric-enheter", - "no_url_available": "Ingen URL tilgjengelig. For informasjon om denne feilen, [sjekk hjelpseksjonen]({docs_url})" + "no_url_available": "Ingen URL tilgjengelig. For informasjon om denne feilen, [sjekk hjelpseksjonen]({docs_url})", + "unknown": "Uventet feil" }, "error": { "cannot_connect": "Tilkobling mislyktes", diff --git a/homeassistant/components/lametric/translations/pl.json b/homeassistant/components/lametric/translations/pl.json index 77e50661cae..8ba3215cbe2 100644 --- a/homeassistant/components/lametric/translations/pl.json +++ b/homeassistant/components/lametric/translations/pl.json @@ -3,22 +3,49 @@ "abort": { "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", "authorize_url_timeout": "Przekroczono limit czasu generowania URL autoryzacji", - "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})" + "invalid_discovery_info": "Otrzymano nieprawid\u0142owe informacje wykrywania.", + "link_local_address": "Po\u0142\u0105czenie lokalnego adresu nie jest obs\u0142ugiwane", + "missing_configuration": "Komponent nie jest skonfigurowany. Post\u0119puj zgodnie z dokumentacj\u0105.", + "no_devices": "Autoryzowany u\u017cytkownik nie posiada urz\u0105dze\u0144 LaMetric", + "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})", + "unknown": "Nieoczekiwany b\u0142\u0105d" }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "unknown": "Nieoczekiwany b\u0142\u0105d" }, "step": { + "choice_enter_manual_or_fetch_cloud": { + "description": "Urz\u0105dzenie LaMetric mo\u017cna skonfigurowa\u0107 w Home Assistant na dwa r\u00f3\u017cne sposoby. \n\nMo\u017cesz samodzielnie wprowadzi\u0107 wszystkie informacje o urz\u0105dzeniu i tokeny API lub Home Assistant mo\u017ce je zaimportowa\u0107 z Twojego konta LaMetric.com.", + "menu_options": { + "manual_entry": "Wprowad\u017a r\u0119cznie", + "pick_implementation": "Importuj z LaMetric.com (zalecane)" + } + }, "manual_entry": { "data": { "api_key": "Klucz API", "host": "Nazwa hosta lub adres IP" + }, + "data_description": { + "api_key": "Ten klucz API znajdziesz na [stronie urz\u0105dze\u0144 na swoim koncie programisty LaMetric](https://developer.lametric.com/user/devices).", + "host": "Adres IP lub nazwa hosta LaMetric TIME w Twojej sieci." } }, "pick_implementation": { "title": "Wybierz metod\u0119 uwierzytelniania" + }, + "user_cloud_select_device": { + "data": { + "device": "Wybierz urz\u0105dzenie LaMetric do dodania" + } } } + }, + "issues": { + "manual_migration": { + "description": "Integracja LaMetric zosta\u0142a zmodernizowana: jest teraz konfigurowana za pomoc\u0105 interfejsu u\u017cytkownika, a komunikacja odbywa si\u0119 lokalnie. \n\nNiestety nie ma mo\u017cliwo\u015bci automatycznej migracji i dlatego wymaga si\u0119 ponownej konfiguracji LaMetric za pomoc\u0105 Home Assistanta. Zapoznaj si\u0119 z dokumentacj\u0105 integracji Home Assistant LaMetric, aby dowiedzie\u0107 si\u0119, jak to skonfigurowa\u0107. \n\nUsu\u0144 konfiguracj\u0119 LaMetric YAML z pliku configuration.yaml i uruchom ponownie Home Assistanta, aby rozwi\u0105za\u0107 ten problem.", + "title": "Wymagana jest r\u0119czna migracja dla LaMetric" + } } } \ No newline at end of file diff --git a/homeassistant/components/lametric/translations/pt-BR.json b/homeassistant/components/lametric/translations/pt-BR.json index 7349baeb8dc..b9834dbfa2f 100644 --- a/homeassistant/components/lametric/translations/pt-BR.json +++ b/homeassistant/components/lametric/translations/pt-BR.json @@ -7,7 +7,8 @@ "link_local_address": "Endere\u00e7os locais de links n\u00e3o s\u00e3o suportados", "missing_configuration": "A integra\u00e7\u00e3o LaMetric n\u00e3o est\u00e1 configurada. Por favor, siga a documenta\u00e7\u00e3o.", "no_devices": "O usu\u00e1rio autorizado n\u00e3o possui dispositivos LaMetric", - "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})" + "no_url_available": "N\u00e3o h\u00e1 URL dispon\u00edvel. Para obter informa\u00e7\u00f5es sobre esse erro, [verifique a se\u00e7\u00e3o de ajuda]({docs_url})", + "unknown": "Erro inesperado" }, "error": { "cannot_connect": "Falha ao conectar", diff --git a/homeassistant/components/lametric/translations/pt.json b/homeassistant/components/lametric/translations/pt.json new file mode 100644 index 00000000000..ca715b2e6e2 --- /dev/null +++ b/homeassistant/components/lametric/translations/pt.json @@ -0,0 +1,36 @@ +{ + "config": { + "abort": { + "invalid_discovery_info": "Informa\u00e7\u00f5es de descoberta inv\u00e1lidas recebidas", + "link_local_address": "Endere\u00e7os locais de link n\u00e3o s\u00e3o suportados", + "missing_configuration": "A integra\u00e7\u00e3o LaMetric n\u00e3o est\u00e1 configurada. Por favor, siga a documenta\u00e7\u00e3o.", + "no_devices": "O usu\u00e1rio autorizado n\u00e3o possui dispositivos LaMetric" + }, + "step": { + "choice_enter_manual_or_fetch_cloud": { + "description": "Um dispositivo LaMetric pode ser configurado no Home Assistant de duas maneiras diferentes. \n\n Voc\u00ea mesmo pode inserir todas as informa\u00e7\u00f5es do dispositivo e tokens de API, ou o Home Assistant pode import\u00e1-los de sua conta LaMetric.com.", + "menu_options": { + "manual_entry": "Entrar manualmente", + "pick_implementation": "Importar do LaMetric.com (recomendado)" + } + }, + "manual_entry": { + "data_description": { + "api_key": "Voc\u00ea pode encontrar essa chave de API em [p\u00e1gina de dispositivos em sua conta de desenvolvedor LaMetric](https://developer.lametric.com/user/devices).", + "host": "O endere\u00e7o IP ou nome de host do seu LaMetric TIME em sua rede." + } + }, + "user_cloud_select_device": { + "data": { + "device": "Selecione o dispositivo LaMetric para adicionar" + } + } + } + }, + "issues": { + "manual_migration": { + "description": "A integra\u00e7\u00e3o LaMetric foi modernizada: agora est\u00e1 configurada e configurada atrav\u00e9s da interface do usu\u00e1rio e as comunica\u00e7\u00f5es agora s\u00e3o locais. \n\n Infelizmente, n\u00e3o h\u00e1 caminho de migra\u00e7\u00e3o autom\u00e1tica poss\u00edvel e, portanto, exige que voc\u00ea reconfigure seu LaMetric com o Home Assistant. Consulte a documenta\u00e7\u00e3o de integra\u00e7\u00e3o do Home Assistant LaMetric sobre como configur\u00e1-lo. \n\n Remova a configura\u00e7\u00e3o antiga do LaMetric YAML do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "Migra\u00e7\u00e3o manual necess\u00e1ria para LaMetric" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lametric/translations/ru.json b/homeassistant/components/lametric/translations/ru.json index 34a1bb58a62..cf9324b9abe 100644 --- a/homeassistant/components/lametric/translations/ru.json +++ b/homeassistant/components/lametric/translations/ru.json @@ -7,7 +7,8 @@ "link_local_address": "\u0421\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u044b\u0435 \u0430\u0434\u0440\u0435\u0441\u0430 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f.", "missing_configuration": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f LaMetric \u043d\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438.", "no_devices": "\u0423 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u043e\u0432\u0430\u043d\u043d\u043e\u0433\u043e \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u043d\u0435\u0442 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 LaMetric.", - "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." + "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.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", diff --git a/homeassistant/components/lametric/translations/sv.json b/homeassistant/components/lametric/translations/sv.json new file mode 100644 index 00000000000..4ea1aa31e3f --- /dev/null +++ b/homeassistant/components/lametric/translations/sv.json @@ -0,0 +1,50 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "authorize_url_timeout": "Timeout vid generering av en auktoriserings-URL.", + "invalid_discovery_info": "Felaktig uppt\u00e4cktsinformation har tagits emot", + "link_local_address": "Lokala l\u00e4nkadresser st\u00f6ds inte", + "missing_configuration": "LaMetric-integrationen \u00e4r inte konfigurerad. V\u00e4nligen f\u00f6lj dokumentationen.", + "no_devices": "Den auktoriserade anv\u00e4ndaren har inga LaMetric-enheter", + "no_url_available": "Ingen webbadress tillg\u00e4nglig. F\u00f6r information om detta fel, [kolla hj\u00e4lpavsnittet]({docs_url})" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "choice_enter_manual_or_fetch_cloud": { + "description": "En LaMetric-enhet kan st\u00e4llas in i Home Assistant p\u00e5 tv\u00e5 olika s\u00e4tt. \n\n Du kan sj\u00e4lv ange all enhetsinformation och API-tokens, eller s\u00e5 kan Home Assistent importera dem fr\u00e5n ditt LaMetric.com-konto.", + "menu_options": { + "manual_entry": "Ange manuellt", + "pick_implementation": "Importera fr\u00e5n LaMetric.com (rekommenderas)" + } + }, + "manual_entry": { + "data": { + "api_key": "API-nyckel", + "host": "V\u00e4rd" + }, + "data_description": { + "api_key": "Du hittar denna API-nyckel p\u00e5 [enhetssidan i ditt LaMetric-utvecklarkonto](https://developer.lametric.com/user/devices).", + "host": "IP-adressen eller v\u00e4rdnamnet f\u00f6r din LaMetric TIME p\u00e5 ditt n\u00e4tverk." + } + }, + "pick_implementation": { + "title": "V\u00e4lj autentiseringsmetod" + }, + "user_cloud_select_device": { + "data": { + "device": "V\u00e4lj den LaMetric-enhet du vill l\u00e4gga till" + } + } + } + }, + "issues": { + "manual_migration": { + "description": "LaMetric-integrationen har moderniserats: den \u00e4r nu konfigurerad och konfigurerad via anv\u00e4ndargr\u00e4nssnittet och kommunikationen \u00e4r nu lokal. \n\n Tyv\u00e4rr finns det ingen automatisk migreringsv\u00e4g m\u00f6jlig och kr\u00e4ver d\u00e4rf\u00f6r att du \u00e5terst\u00e4ller din LaMetric med Home Assistant. Se Home Assistant LaMetric-integreringsdokumentationen om hur du st\u00e4ller in den. \n\n Ta bort den gamla LaMetric YAML-konfigurationen fr\u00e5n filen configuration.yaml och starta om Home Assistant f\u00f6r att \u00e5tg\u00e4rda problemet.", + "title": "Manuell migrering kr\u00e4vs f\u00f6r LaMetric" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lametric/translations/zh-Hant.json b/homeassistant/components/lametric/translations/zh-Hant.json index e9c2835756c..bcaa67ed4ad 100644 --- a/homeassistant/components/lametric/translations/zh-Hant.json +++ b/homeassistant/components/lametric/translations/zh-Hant.json @@ -7,7 +7,8 @@ "link_local_address": "\u4e0d\u652f\u63f4\u9023\u7d50\u672c\u5730\u7aef\u4f4d\u5740", "missing_configuration": "LaMetric \u6574\u5408\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002", "no_devices": "\u8a8d\u8b49\u4f7f\u7528\u8005\u6c92\u6709\u4efb\u4f55 LaMetric \u88dd\u7f6e", - "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})" + "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})", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/landisgyr_heat_meter/const.py b/homeassistant/components/landisgyr_heat_meter/const.py index 55a6c65892c..57a8f9d9be4 100644 --- a/homeassistant/components/landisgyr_heat_meter/const.py +++ b/homeassistant/components/landisgyr_heat_meter/const.py @@ -44,6 +44,14 @@ HEAT_METER_SENSOR_TYPES = ( native_unit_of_measurement=ENERGY_MEGA_WATT_HOUR, entity_category=EntityCategory.DIAGNOSTIC, ), + # Diagnostic entity for debugging, this will match the value in GJ of previous year indicated on the meter's display + SensorEntityDescription( + key="heat_previous_year_gj", + icon="mdi:fire", + name="Heat previous year GJ", + native_unit_of_measurement="GJ", + entity_category=EntityCategory.DIAGNOSTIC, + ), SensorEntityDescription( key="volume_previous_year_m3", icon="mdi:fire", diff --git a/homeassistant/components/landisgyr_heat_meter/manifest.json b/homeassistant/components/landisgyr_heat_meter/manifest.json index 359ca1acea6..9d1faa570b7 100644 --- a/homeassistant/components/landisgyr_heat_meter/manifest.json +++ b/homeassistant/components/landisgyr_heat_meter/manifest.json @@ -3,7 +3,7 @@ "name": "Landis+Gyr Heat Meter", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/landisgyr_heat_meter", - "requirements": ["ultraheat-api==0.4.1"], + "requirements": ["ultraheat-api==0.4.3"], "ssdp": [], "zeroconf": [], "homekit": {}, diff --git a/homeassistant/components/landisgyr_heat_meter/sensor.py b/homeassistant/components/landisgyr_heat_meter/sensor.py index 1d38b1f5816..23a6e217458 100644 --- a/homeassistant/components/landisgyr_heat_meter/sensor.py +++ b/homeassistant/components/landisgyr_heat_meter/sensor.py @@ -73,7 +73,7 @@ class HeatMeterSensor(CoordinatorEntity, RestoreSensor): self._attr_device_info = device self._attr_should_poll = bool(self.key in ("heat_usage", "heat_previous_year")) - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Call when entity about to be added to hass.""" await super().async_added_to_hass() state = await self.async_get_last_sensor_data() diff --git a/homeassistant/components/landisgyr_heat_meter/translations/bg.json b/homeassistant/components/landisgyr_heat_meter/translations/bg.json new file mode 100644 index 00000000000..6120c9152d7 --- /dev/null +++ b/homeassistant/components/landisgyr_heat_meter/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", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "user": { + "data": { + "device": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/landisgyr_heat_meter/translations/cs.json b/homeassistant/components/landisgyr_heat_meter/translations/cs.json new file mode 100644 index 00000000000..500211d103c --- /dev/null +++ b/homeassistant/components/landisgyr_heat_meter/translations/cs.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "setup_serial_manual_path": { + "data": { + "device": "Cesta k USB za\u0159\u00edzen\u00ed" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/landisgyr_heat_meter/translations/pl.json b/homeassistant/components/landisgyr_heat_meter/translations/pl.json index 8dcc31a5a11..3478a5c0c2b 100644 --- a/homeassistant/components/landisgyr_heat_meter/translations/pl.json +++ b/homeassistant/components/landisgyr_heat_meter/translations/pl.json @@ -12,6 +12,11 @@ "data": { "device": "\u015acie\u017cka urz\u0105dzenia USB" } + }, + "user": { + "data": { + "device": "Wybierz urz\u0105dzenie" + } } } } diff --git a/homeassistant/components/landisgyr_heat_meter/translations/sv.json b/homeassistant/components/landisgyr_heat_meter/translations/sv.json new file mode 100644 index 00000000000..4fde3cf2755 --- /dev/null +++ b/homeassistant/components/landisgyr_heat_meter/translations/sv.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "setup_serial_manual_path": { + "data": { + "device": "USB-enhetens s\u00f6kv\u00e4g" + } + }, + "user": { + "data": { + "device": "V\u00e4lj enhet" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lastfm/sensor.py b/homeassistant/components/lastfm/sensor.py index 9c158b244cb..2675371f033 100644 --- a/homeassistant/components/lastfm/sensor.py +++ b/homeassistant/components/lastfm/sensor.py @@ -91,7 +91,7 @@ class LastfmSensor(SensorEntity): """Return the state of the sensor.""" return self._state - def update(self): + def update(self) -> None: """Update device state.""" self._cover = self._user.get_image() self._playcount = self._user.get_playcount() @@ -101,10 +101,11 @@ class LastfmSensor(SensorEntity): self._lastplayed = f"{last.track.artist} - {last.track.title}" if top_tracks := self._user.get_top_tracks(limit=1): - top = top_tracks[0] - toptitle = re.search("', '(.+?)',", str(top)) - topartist = re.search("'(.+?)',", str(top)) - self._topplayed = f"{topartist.group(1)} - {toptitle.group(1)}" + top = str(top_tracks[0]) + if (toptitle := re.search("', '(.+?)',", top)) and ( + topartist := re.search("'(.+?)',", top) + ): + self._topplayed = f"{topartist.group(1)} - {toptitle.group(1)}" if (now_playing := self._user.get_now_playing()) is None: self._state = STATE_NOT_SCROBBLING diff --git a/homeassistant/components/laundrify/translations/cs.json b/homeassistant/components/laundrify/translations/cs.json new file mode 100644 index 00000000000..9eecd724896 --- /dev/null +++ b/homeassistant/components/laundrify/translations/cs.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "reauth_confirm": { + "title": "Znovu ov\u011b\u0159it integraci" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lcn/climate.py b/homeassistant/components/lcn/climate.py index 31aedab2fe6..8d701fbfa2f 100644 --- a/homeassistant/components/lcn/climate.py +++ b/homeassistant/components/lcn/climate.py @@ -5,8 +5,12 @@ from typing import Any, cast import pypck -from homeassistant.components.climate import DOMAIN as DOMAIN_CLIMATE, ClimateEntity -from homeassistant.components.climate.const import ClimateEntityFeature, HVACMode +from homeassistant.components.climate import ( + DOMAIN as DOMAIN_CLIMATE, + ClimateEntity, + ClimateEntityFeature, + HVACMode, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_TEMPERATURE, diff --git a/homeassistant/components/lcn/translations/id.json b/homeassistant/components/lcn/translations/id.json index e265e2e357b..f728f67fdfd 100644 --- a/homeassistant/components/lcn/translations/id.json +++ b/homeassistant/components/lcn/translations/id.json @@ -1,8 +1,9 @@ { "device_automation": { "trigger_type": { + "codelock": "kode kunci diterima", "fingerprint": "kode sidik jari diterima", - "send_keys": "kode dikirim diterima", + "send_keys": "kunci kirim diterima", "transmitter": "kode pemancar diterima", "transponder": "kode transponder diterima" } diff --git a/homeassistant/components/lcn/translations/ja.json b/homeassistant/components/lcn/translations/ja.json index 30849a56dc5..17b7ab9b0f1 100644 --- a/homeassistant/components/lcn/translations/ja.json +++ b/homeassistant/components/lcn/translations/ja.json @@ -5,7 +5,7 @@ "fingerprint": "\u6307\u7d0b\u30b3\u30fc\u30c9\u3092\u53d7\u4fe1\u3057\u307e\u3057\u305f(fingerprint code received)", "send_keys": "\u9001\u4fe1\u30ad\u30fc\u3092\u53d7\u4fe1\u3057\u307e\u3057\u305f(send keys received)", "transmitter": "\u9001\u4fe1\u6a5f\u30b3\u30fc\u30c9\u53d7\u4fe1\u3057\u307e\u3057\u305f(transmitter code received)", - "transponder": "\u30c8\u30e9\u30f3\u30b9\u30dd\u30fc\u30c0\u30fc\u30b3\u30fc\u30c9\u3092\u53d7\u4fe1\u3057\u307e\u3057\u305f(transpoder code received)" + "transponder": "\u30c8\u30e9\u30f3\u30b9\u30dd\u30f3\u30c0\u30fc\u30b3\u30fc\u30c9\u3092\u53d7\u4fe1" } } } \ No newline at end of file diff --git a/homeassistant/components/led_ble/translations/bg.json b/homeassistant/components/led_ble/translations/bg.json new file mode 100644 index 00000000000..58e74997565 --- /dev/null +++ b/homeassistant/components/led_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 \u043e\u0442\u043a\u0440\u0438\u0442\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/led_ble/translations/cs.json b/homeassistant/components/led_ble/translations/cs.json new file mode 100644 index 00000000000..99738ebc78e --- /dev/null +++ b/homeassistant/components/led_ble/translations/cs.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "already_in_progress": "Konfigurace ji\u017e prob\u00edh\u00e1", + "no_devices_found": "V s\u00edti nebyla nalezena \u017e\u00e1dn\u00e1 za\u0159\u00edzen\u00ed" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/led_ble/translations/el.json b/homeassistant/components/led_ble/translations/el.json new file mode 100644 index 00000000000..ad4b994de80 --- /dev/null +++ b/homeassistant/components/led_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 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\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/led_ble/translations/hu.json b/homeassistant/components/led_ble/translations/hu.json new file mode 100644 index 00000000000..6b6e576abcc --- /dev/null +++ b/homeassistant/components/led_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/led_ble/translations/id.json b/homeassistant/components/led_ble/translations/id.json new file mode 100644 index 00000000000..80afbbcf132 --- /dev/null +++ b/homeassistant/components/led_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/led_ble/translations/ja.json b/homeassistant/components/led_ble/translations/ja.json new file mode 100644 index 00000000000..4fee9ec6904 --- /dev/null +++ b/homeassistant/components/led_ble/translations/ja.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "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", + "no_unconfigured_devices": "\u672a\u69cb\u6210\u306e\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3002", + "not_supported": "\u30c7\u30d0\u30a4\u30b9\u306f\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\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/led_ble/translations/nl.json b/homeassistant/components/led_ble/translations/nl.json new file mode 100644 index 00000000000..14d596f2d6b --- /dev/null +++ b/homeassistant/components/led_ble/translations/nl.json @@ -0,0 +1,23 @@ +{ + "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", + "no_unconfigured_devices": "Geen niet-geconfigureerde apparaten gevonden.", + "not_supported": "Apparaat is 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/led_ble/translations/pl.json b/homeassistant/components/led_ble/translations/pl.json index b59a7e3bc02..44100e63777 100644 --- a/homeassistant/components/led_ble/translations/pl.json +++ b/homeassistant/components/led_ble/translations/pl.json @@ -4,12 +4,20 @@ "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}" + "flow_title": "{name}", + "step": { + "user": { + "data": { + "address": "Adres Bluetooth" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/led_ble/translations/pt.json b/homeassistant/components/led_ble/translations/pt.json new file mode 100644 index 00000000000..c7268106706 --- /dev/null +++ b/homeassistant/components/led_ble/translations/pt.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_unconfigured_devices": "Nenhum dispositivo n\u00e3o configurado encontrado.", + "not_supported": "Dispositivo n\u00e3o suportado" + }, + "step": { + "user": { + "data": { + "address": "Endere\u00e7o Bluetooth" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/led_ble/translations/sv.json b/homeassistant/components/led_ble/translations/sv.json new file mode 100644 index 00000000000..0e45348e74d --- /dev/null +++ b/homeassistant/components/led_ble/translations/sv.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "already_in_progress": "Konfigurationsfl\u00f6det p\u00e5g\u00e5r redan", + "no_devices_found": "Inga enheter hittades i n\u00e4tverket", + "no_unconfigured_devices": "Inga okonfigurerade enheter hittades.", + "not_supported": "Enheten st\u00f6ds inte" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "unknown": "Ov\u00e4ntat fel" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "address": "Bluetooth-adress" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/led_ble/translations/tr.json b/homeassistant/components/led_ble/translations/tr.json new file mode 100644 index 00000000000..f9755124974 --- /dev/null +++ b/homeassistant/components/led_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/lg_netcast/media_player.py b/homeassistant/components/lg_netcast/media_player.py index a36ac83d37c..6f3508e22eb 100644 --- a/homeassistant/components/lg_netcast/media_player.py +++ b/homeassistant/components/lg_netcast/media_player.py @@ -2,8 +2,9 @@ from __future__ import annotations from datetime import datetime +from typing import Any -from pylgnetcast import LgNetCastClient, LgNetCastError +from pylgnetcast import LG_COMMAND, LgNetCastClient, LgNetCastError from requests import RequestException import voluptuous as vol @@ -12,16 +13,10 @@ from homeassistant.components.media_player import ( MediaPlayerDeviceClass, MediaPlayerEntity, MediaPlayerEntityFeature, + MediaPlayerState, + MediaType, ) -from homeassistant.components.media_player.const import MEDIA_TYPE_CHANNEL -from homeassistant.const import ( - CONF_ACCESS_TOKEN, - CONF_HOST, - CONF_NAME, - STATE_OFF, - STATE_PAUSED, - STATE_PLAYING, -) +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST, CONF_NAME from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -80,6 +75,7 @@ class LgTVDevice(MediaPlayerEntity): """Representation of a LG TV.""" _attr_device_class = MediaPlayerDeviceClass.TV + _attr_media_content_type = MediaType.CHANNEL def __init__(self, client, name, on_action_script): """Initialize the LG TV device.""" @@ -104,14 +100,14 @@ class LgTVDevice(MediaPlayerEntity): with self._client as client: client.send_command(command) except (LgNetCastError, RequestException): - self._state = STATE_OFF + self._state = MediaPlayerState.OFF - def update(self): + def update(self) -> None: """Retrieve the latest data from the LG TV.""" try: with self._client as client: - self._state = STATE_PLAYING + self._state = MediaPlayerState.PLAYING self.__update_volume() @@ -146,7 +142,7 @@ class LgTVDevice(MediaPlayerEntity): ) self._source_names = [n for n, k in sorted_sources] except (LgNetCastError, RequestException): - self._state = STATE_OFF + self._state = MediaPlayerState.OFF def __update_volume(self): volume_info = self._client.get_volume() @@ -190,11 +186,6 @@ class LgTVDevice(MediaPlayerEntity): """Content id of current playing media.""" return self._channel_id - @property - def media_content_type(self): - """Content type of current playing media.""" - return MEDIA_TYPE_CHANNEL - @property def media_channel(self): """Channel currently playing.""" @@ -219,65 +210,65 @@ class LgTVDevice(MediaPlayerEntity): f"{self._client.url}data?target=screen_image&_={datetime.now().timestamp()}" ) - def turn_off(self): + def turn_off(self) -> None: """Turn off media player.""" - self.send_command(1) + self.send_command(LG_COMMAND.POWER) - def turn_on(self): + def turn_on(self) -> None: """Turn on the media player.""" if self._on_action_script: self._on_action_script.run(context=self._context) - def volume_up(self): + def volume_up(self) -> None: """Volume up the media player.""" - self.send_command(24) + self.send_command(LG_COMMAND.VOLUME_UP) - def volume_down(self): + def volume_down(self) -> None: """Volume down media player.""" - self.send_command(25) + self.send_command(LG_COMMAND.VOLUME_DOWN) - def set_volume_level(self, volume): + def set_volume_level(self, volume: float) -> None: """Set volume level, range 0..1.""" self._client.set_volume(float(volume * 100)) - def mute_volume(self, mute): + def mute_volume(self, mute: bool) -> None: """Send mute command.""" - self.send_command(26) + self.send_command(LG_COMMAND.MUTE_TOGGLE) - def select_source(self, source): + def select_source(self, source: str) -> None: """Select input source.""" self._client.change_channel(self._sources[source]) - def media_play_pause(self): + def media_play_pause(self) -> None: """Simulate play pause media player.""" if self._playing: self.media_pause() else: self.media_play() - def media_play(self): + def media_play(self) -> None: """Send play command.""" self._playing = True - self._state = STATE_PLAYING - self.send_command(33) + self._state = MediaPlayerState.PLAYING + self.send_command(LG_COMMAND.PLAY) - def media_pause(self): + def media_pause(self) -> None: """Send media pause command to media player.""" self._playing = False - self._state = STATE_PAUSED - self.send_command(34) + self._state = MediaPlayerState.PAUSED + self.send_command(LG_COMMAND.PAUSE) - def media_next_track(self): + def media_next_track(self) -> None: """Send next track command.""" - self.send_command(36) + self.send_command(LG_COMMAND.FAST_FORWARD) - def media_previous_track(self): + def media_previous_track(self) -> None: """Send the previous track command.""" - self.send_command(37) + self.send_command(LG_COMMAND.REWIND) - def play_media(self, media_type, media_id, **kwargs): + def play_media(self, media_type: str, media_id: str, **kwargs: Any) -> None: """Tune to channel.""" - if media_type != MEDIA_TYPE_CHANNEL: + if media_type != MediaType.CHANNEL: raise ValueError(f"Invalid media type: {media_type}") for name, channel in self._sources.items(): diff --git a/homeassistant/components/lg_soundbar/media_player.py b/homeassistant/components/lg_soundbar/media_player.py index 941042d5bce..c4491a1d257 100644 --- a/homeassistant/components/lg_soundbar/media_player.py +++ b/homeassistant/components/lg_soundbar/media_player.py @@ -6,9 +6,10 @@ import temescal from homeassistant.components.media_player import ( MediaPlayerEntity, MediaPlayerEntityFeature, + MediaPlayerState, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST, CONF_PORT, STATE_ON +from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -34,6 +35,7 @@ class LGDevice(MediaPlayerEntity): """Representation of an LG soundbar device.""" _attr_should_poll = False + _attr_state = MediaPlayerState.ON _attr_supported_features = ( MediaPlayerEntityFeature.VOLUME_SET | MediaPlayerEntityFeature.VOLUME_MUTE @@ -65,11 +67,11 @@ class LGDevice(MediaPlayerEntity): self._treble = 0 self._device = None - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register the callback after hass is ready for it.""" await self.hass.async_add_executor_job(self._connect) - def _connect(self): + def _connect(self) -> None: """Perform the actual devices setup.""" self._device = temescal.temescal( self._host, port=self._port, callback=self.handle_event @@ -126,7 +128,7 @@ class LGDevice(MediaPlayerEntity): self.schedule_update_ha_state() - def update(self): + def update(self) -> None: """Trigger updates from the device.""" self._device.get_eq() self._device.get_info() @@ -145,11 +147,6 @@ class LGDevice(MediaPlayerEntity): """Boolean if volume is currently muted.""" return self._mute - @property - def state(self): - """Return the state of the device.""" - return STATE_ON - @property def sound_mode(self): """Return the current sound mode.""" @@ -182,19 +179,19 @@ class LGDevice(MediaPlayerEntity): sources.append(temescal.functions[function]) return sorted(sources) - def set_volume_level(self, volume): + def set_volume_level(self, volume: float) -> None: """Set volume level, range 0..1.""" volume = volume * self._volume_max self._device.set_volume(int(volume)) - def mute_volume(self, mute): + def mute_volume(self, mute: bool) -> None: """Mute (true) or unmute (false) media player.""" self._device.set_mute(mute) - def select_source(self, source): + def select_source(self, source: str) -> None: """Select input source.""" self._device.set_func(temescal.functions.index(source)) - def select_sound_mode(self, sound_mode): + def select_sound_mode(self, sound_mode: str) -> None: """Set Sound Mode for Receiver..""" self._device.set_eq(temescal.equalisers.index(sound_mode)) diff --git a/homeassistant/components/lg_soundbar/translations/bg.json b/homeassistant/components/lg_soundbar/translations/bg.json new file mode 100644 index 00000000000..5d235f77133 --- /dev/null +++ b/homeassistant/components/lg_soundbar/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": { + "host": "\u0425\u043e\u0441\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lg_soundbar/translations/cs.json b/homeassistant/components/lg_soundbar/translations/cs.json new file mode 100644 index 00000000000..6fabc170b6e --- /dev/null +++ b/homeassistant/components/lg_soundbar/translations/cs.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" + }, + "step": { + "user": { + "data": { + "host": "Hostitel" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lidarr/__init__.py b/homeassistant/components/lidarr/__init__.py new file mode 100644 index 00000000000..6410e520b42 --- /dev/null +++ b/homeassistant/components/lidarr/__init__.py @@ -0,0 +1,85 @@ +"""The Lidarr component.""" +from __future__ import annotations + +from aiopyarr.lidarr_client import LidarrClient +from aiopyarr.models.host_configuration import PyArrHostConfiguration + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_API_KEY, CONF_URL, CONF_VERIFY_SSL, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.device_registry import DeviceEntryType +from homeassistant.helpers.entity import DeviceInfo, EntityDescription +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DEFAULT_NAME, DOMAIN +from .coordinator import ( + DiskSpaceDataUpdateCoordinator, + LidarrDataUpdateCoordinator, + QueueDataUpdateCoordinator, + StatusDataUpdateCoordinator, + WantedDataUpdateCoordinator, +) + +PLATFORMS = [Platform.SENSOR] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Lidarr from a config entry.""" + host_configuration = PyArrHostConfiguration( + api_token=entry.data[CONF_API_KEY], + verify_ssl=entry.data[CONF_VERIFY_SSL], + url=entry.data[CONF_URL], + ) + lidarr = LidarrClient( + host_configuration=host_configuration, + session=async_get_clientsession(hass, host_configuration.verify_ssl), + request_timeout=60, + ) + coordinators: dict[str, LidarrDataUpdateCoordinator] = { + "disk_space": DiskSpaceDataUpdateCoordinator(hass, host_configuration, lidarr), + "queue": QueueDataUpdateCoordinator(hass, host_configuration, lidarr), + "status": StatusDataUpdateCoordinator(hass, host_configuration, lidarr), + "wanted": WantedDataUpdateCoordinator(hass, host_configuration, lidarr), + } + # Temporary, until we add diagnostic entities + _version = None + for coordinator in coordinators.values(): + await coordinator.async_config_entry_first_refresh() + if isinstance(coordinator, StatusDataUpdateCoordinator): + _version = coordinator.data + coordinator.system_version = _version + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinators + 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 LidarrEntity(CoordinatorEntity[LidarrDataUpdateCoordinator]): + """Defines a base Lidarr entity.""" + + _attr_has_entity_name = True + + def __init__( + self, coordinator: LidarrDataUpdateCoordinator, description: EntityDescription + ) -> None: + """Initialize the Lidarr entity.""" + super().__init__(coordinator) + self.entity_description = description + self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{description.key}" + self._attr_device_info = DeviceInfo( + configuration_url=coordinator.host_configuration.base_url, + entry_type=DeviceEntryType.SERVICE, + identifiers={(DOMAIN, coordinator.config_entry.entry_id)}, + manufacturer=DEFAULT_NAME, + name=DEFAULT_NAME, + sw_version=coordinator.system_version, + ) diff --git a/homeassistant/components/lidarr/config_flow.py b/homeassistant/components/lidarr/config_flow.py new file mode 100644 index 00000000000..a0b73950766 --- /dev/null +++ b/homeassistant/components/lidarr/config_flow.py @@ -0,0 +1,113 @@ +"""Config flow for Lidarr.""" +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any + +from aiohttp import ClientConnectorError +from aiopyarr import exceptions +from aiopyarr.lidarr_client import LidarrClient +import voluptuous as vol + +from homeassistant.config_entries import ConfigEntry, ConfigFlow +from homeassistant.const import CONF_API_KEY, CONF_URL, CONF_VERIFY_SSL +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import DEFAULT_NAME, DOMAIN + + +class LidarrConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for Lidarr.""" + + VERSION = 1 + + def __init__(self) -> None: + """Initialize the flow.""" + self.entry: ConfigEntry | None = None + + async def async_step_reauth(self, user_input: Mapping[str, Any]) -> FlowResult: + """Handle configuration by re-auth.""" + self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) + + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: dict[str, str] | None = None + ) -> FlowResult: + """Confirm reauth dialog.""" + if user_input is not None: + return await self.async_step_user() + + self._set_confirm_only() + return self.async_show_form(step_id="reauth_confirm") + + 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 None: + user_input = dict(self.entry.data) if self.entry else None + + else: + try: + if result := await validate_input(self.hass, user_input): + user_input[CONF_API_KEY] = result[1] + except exceptions.ArrAuthenticationException: + errors = {"base": "invalid_auth"} + except (ClientConnectorError, exceptions.ArrConnectionException): + errors = {"base": "cannot_connect"} + except exceptions.ArrWrongAppException: + errors = {"base": "wrong_app"} + except exceptions.ArrZeroConfException: + errors = {"base": "zeroconf_failed"} + except exceptions.ArrException: + errors = {"base": "unknown"} + if not errors: + if self.entry: + self.hass.config_entries.async_update_entry( + self.entry, data=user_input + ) + await self.hass.config_entries.async_reload(self.entry.entry_id) + + return self.async_abort(reason="reauth_successful") + + return self.async_create_entry(title=DEFAULT_NAME, data=user_input) + + user_input = user_input or {} + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_URL, default=user_input.get(CONF_URL, "")): str, + vol.Optional(CONF_API_KEY): str, + vol.Optional( + CONF_VERIFY_SSL, + default=user_input.get(CONF_VERIFY_SSL, False), + ): bool, + } + ), + errors=errors, + ) + + +async def validate_input( + hass: HomeAssistant, data: dict[str, Any] +) -> tuple[str, str, str] | None: + """Validate the user input allows us to connect. + + Data has the keys from DATA_SCHEMA with values provided by the user. + """ + lidarr = LidarrClient( + api_token=data.get(CONF_API_KEY, ""), + url=data[CONF_URL], + session=async_get_clientsession(hass), + verify_ssl=data[CONF_VERIFY_SSL], + ) + if CONF_API_KEY not in data: + return await lidarr.async_try_zeroconf() + await lidarr.async_get_system_status() + return None diff --git a/homeassistant/components/lidarr/const.py b/homeassistant/components/lidarr/const.py new file mode 100644 index 00000000000..08e284b9b31 --- /dev/null +++ b/homeassistant/components/lidarr/const.py @@ -0,0 +1,38 @@ +"""Constants for Lidarr.""" +import logging +from typing import Final + +from homeassistant.const import ( + DATA_BYTES, + DATA_EXABYTES, + DATA_GIGABYTES, + DATA_KILOBYTES, + DATA_MEGABYTES, + DATA_PETABYTES, + DATA_TERABYTES, + DATA_YOTTABYTES, + DATA_ZETTABYTES, +) + +BYTE_SIZES = [ + DATA_BYTES, + DATA_KILOBYTES, + DATA_MEGABYTES, + DATA_GIGABYTES, + DATA_TERABYTES, + DATA_PETABYTES, + DATA_EXABYTES, + DATA_ZETTABYTES, + DATA_YOTTABYTES, +] + +# Defaults +DEFAULT_DAYS = "1" +DEFAULT_HOST = "localhost" +DEFAULT_NAME = "Lidarr" +DEFAULT_UNIT = DATA_GIGABYTES +DEFAULT_MAX_RECORDS = 20 + +DOMAIN: Final = "lidarr" + +LOGGER = logging.getLogger(__package__) diff --git a/homeassistant/components/lidarr/coordinator.py b/homeassistant/components/lidarr/coordinator.py new file mode 100644 index 00000000000..be789c6a32a --- /dev/null +++ b/homeassistant/components/lidarr/coordinator.py @@ -0,0 +1,94 @@ +"""Data update coordinator for the Lidarr integration.""" +from __future__ import annotations + +from abc import abstractmethod +from datetime import timedelta +from typing import Generic, TypeVar, cast + +from aiopyarr import LidarrAlbum, LidarrQueue, LidarrRootFolder, exceptions +from aiopyarr.lidarr_client import LidarrClient +from aiopyarr.models.host_configuration import PyArrHostConfiguration + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import DEFAULT_MAX_RECORDS, DOMAIN, LOGGER + +T = TypeVar("T", list[LidarrRootFolder], LidarrQueue, str, LidarrAlbum) + + +class LidarrDataUpdateCoordinator(DataUpdateCoordinator, Generic[T]): + """Data update coordinator for the Lidarr integration.""" + + config_entry: ConfigEntry + + def __init__( + self, + hass: HomeAssistant, + host_configuration: PyArrHostConfiguration, + api_client: LidarrClient, + ) -> None: + """Initialize the coordinator.""" + super().__init__( + hass=hass, + logger=LOGGER, + name=DOMAIN, + update_interval=timedelta(seconds=30), + ) + self.api_client = api_client + self.host_configuration = host_configuration + self.system_version: str | None = None + + async def _async_update_data(self) -> T: + """Get the latest data from Lidarr.""" + try: + return await self._fetch_data() + + except exceptions.ArrConnectionException as ex: + raise UpdateFailed(ex) from ex + except exceptions.ArrAuthenticationException as ex: + raise ConfigEntryAuthFailed( + "API Key is no longer valid. Please reauthenticate" + ) from ex + + @abstractmethod + async def _fetch_data(self) -> T: + """Fetch the actual data.""" + raise NotImplementedError + + +class DiskSpaceDataUpdateCoordinator(LidarrDataUpdateCoordinator): + """Disk space update coordinator for Lidarr.""" + + async def _fetch_data(self) -> list[LidarrRootFolder]: + """Fetch the data.""" + return cast(list, await self.api_client.async_get_root_folders()) + + +class QueueDataUpdateCoordinator(LidarrDataUpdateCoordinator): + """Queue update coordinator.""" + + async def _fetch_data(self) -> LidarrQueue: + """Fetch the album count in queue.""" + return await self.api_client.async_get_queue(page_size=DEFAULT_MAX_RECORDS) + + +class StatusDataUpdateCoordinator(LidarrDataUpdateCoordinator): + """Status update coordinator for Lidarr.""" + + async def _fetch_data(self) -> str: + """Fetch the data.""" + return (await self.api_client.async_get_system_status()).version + + +class WantedDataUpdateCoordinator(LidarrDataUpdateCoordinator): + """Wanted update coordinator.""" + + async def _fetch_data(self) -> LidarrAlbum: + """Fetch the wanted data.""" + return cast( + LidarrAlbum, + await self.api_client.async_get_wanted(page_size=DEFAULT_MAX_RECORDS), + ) diff --git a/homeassistant/components/lidarr/manifest.json b/homeassistant/components/lidarr/manifest.json new file mode 100644 index 00000000000..7d4e9bcede7 --- /dev/null +++ b/homeassistant/components/lidarr/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "lidarr", + "name": "Lidarr", + "documentation": "https://www.home-assistant.io/integrations/lidarr", + "requirements": ["aiopyarr==22.9.0"], + "codeowners": ["@tkdrob"], + "config_flow": true, + "iot_class": "local_polling", + "loggers": ["aiopyarr"] +} diff --git a/homeassistant/components/lidarr/sensor.py b/homeassistant/components/lidarr/sensor.py new file mode 100644 index 00000000000..8529d9a6469 --- /dev/null +++ b/homeassistant/components/lidarr/sensor.py @@ -0,0 +1,162 @@ +"""Support for Lidarr.""" +from __future__ import annotations + +from collections.abc import Callable +from copy import deepcopy +from dataclasses import dataclass +from datetime import datetime +from typing import Generic + +from aiopyarr import LidarrQueueItem, LidarrRootFolder + +from homeassistant.components.sensor import ( + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import DATA_GIGABYTES +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType + +from . import LidarrEntity +from .const import BYTE_SIZES, DOMAIN +from .coordinator import LidarrDataUpdateCoordinator, T + + +def get_space(data: list[LidarrRootFolder], name: str) -> str: + """Get space.""" + space = [] + for mount in data: + if name in mount.path: + mount.freeSpace = mount.freeSpace if mount.accessible else 0 + space.append(mount.freeSpace / 1024 ** BYTE_SIZES.index(DATA_GIGABYTES)) + return f"{space[0]:.2f}" + + +def get_modified_description( + description: LidarrSensorEntityDescription, mount: LidarrRootFolder +) -> tuple[LidarrSensorEntityDescription, str]: + """Return modified description and folder name.""" + desc = deepcopy(description) + name = mount.path.rsplit("/")[-1].rsplit("\\")[-1] + desc.key = f"{description.key}_{name}" + desc.name = f"{description.name} {name}".capitalize() + return desc, name + + +@dataclass +class LidarrSensorEntityDescriptionMixIn(Generic[T]): + """Mixin for required keys.""" + + value_fn: Callable[[T, str], str] + + +@dataclass +class LidarrSensorEntityDescription( + SensorEntityDescription, LidarrSensorEntityDescriptionMixIn, Generic[T] +): + """Class to describe a Lidarr sensor.""" + + attributes_fn: Callable[ + [T], dict[str, StateType | datetime] | None + ] = lambda _: None + description_fn: Callable[ + [LidarrSensorEntityDescription, LidarrRootFolder], + tuple[LidarrSensorEntityDescription, str] | None, + ] = lambda _, __: None + + +SENSOR_TYPES: dict[str, LidarrSensorEntityDescription] = { + "disk_space": LidarrSensorEntityDescription( + key="disk_space", + name="Disk space", + native_unit_of_measurement=DATA_GIGABYTES, + icon="mdi:harddisk", + value_fn=get_space, + state_class=SensorStateClass.TOTAL, + description_fn=get_modified_description, + ), + "queue": LidarrSensorEntityDescription( + key="queue", + name="Queue", + native_unit_of_measurement="Albums", + icon="mdi:download", + value_fn=lambda data, _: data.totalRecords, + state_class=SensorStateClass.TOTAL, + attributes_fn=lambda data: {i.title: queue_str(i) for i in data.records}, + ), + "wanted": LidarrSensorEntityDescription( + key="wanted", + name="Wanted", + native_unit_of_measurement="Albums", + icon="mdi:music", + value_fn=lambda data, _: data.totalRecords, + state_class=SensorStateClass.TOTAL, + entity_registry_enabled_default=False, + attributes_fn=lambda data: { + album.title: album.artist.artistName for album in data.records + }, + ), +} + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Lidarr sensors based on a config entry.""" + coordinators: dict[str, LidarrDataUpdateCoordinator] = hass.data[DOMAIN][ + entry.entry_id + ] + entities = [] + for coordinator_type, description in SENSOR_TYPES.items(): + coordinator = coordinators[coordinator_type] + if coordinator_type != "disk_space": + entities.append(LidarrSensor(coordinator, description)) + else: + entities.extend( + LidarrSensor(coordinator, *get_modified_description(description, mount)) + for mount in coordinator.data + if description.description_fn + ) + async_add_entities(entities) + + +class LidarrSensor(LidarrEntity, SensorEntity): + """Implementation of the Lidarr sensor.""" + + entity_description: LidarrSensorEntityDescription + + def __init__( + self, + coordinator: LidarrDataUpdateCoordinator, + description: LidarrSensorEntityDescription, + folder_name: str = "", + ) -> None: + """Create Lidarr entity.""" + super().__init__(coordinator, description) + self.folder_name = folder_name + + @property + def extra_state_attributes(self) -> dict[str, StateType | datetime] | None: + """Return the state attributes of the sensor.""" + return self.entity_description.attributes_fn(self.coordinator.data) + + @property + def native_value(self) -> StateType: + """Return the state of the sensor.""" + return self.entity_description.value_fn(self.coordinator.data, self.folder_name) + + +def queue_str(item: LidarrQueueItem) -> str: + """Return string description of queue item.""" + if ( + item.sizeleft > 0 + and item.timeleft == "00:00:00" + or not hasattr(item, "trackedDownloadState") + ): + return "stopped" + return item.trackedDownloadState diff --git a/homeassistant/components/lidarr/strings.json b/homeassistant/components/lidarr/strings.json new file mode 100644 index 00000000000..662d930cbef --- /dev/null +++ b/homeassistant/components/lidarr/strings.json @@ -0,0 +1,42 @@ +{ + "config": { + "step": { + "user": { + "description": "API key can be retrieved automatically if login credentials were not set in application.\nYour API key can be found in Settings > General in the Lidarr Web UI.", + "data": { + "api_key": "[%key:common::config_flow::data::api_key%]", + "url": "[%key:common::config_flow::data::url%]", + "verify_ssl": "[%key:common::config_flow::data::verify_ssl%]" + } + }, + "reauth_confirm": { + "title": "[%key:common::config_flow::title::reauth%]", + "description": "The Lidarr integration needs to be manually re-authenticated with the Lidarr API", + "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%]", + "zeroconf_failed": "API key not found. Please enter it manually", + "wrong_app": "Incorrect application reached. Please try again", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" + } + }, + "options": { + "step": { + "init": { + "data": { + "upcoming_days": "Number of upcoming days to display on calendar", + "max_records": "Number of maximum records to display on wanted and queue" + } + } + } + } +} diff --git a/homeassistant/components/lidarr/translations/bg.json b/homeassistant/components/lidarr/translations/bg.json new file mode 100644 index 00000000000..4e22178a11d --- /dev/null +++ b/homeassistant/components/lidarr/translations/bg.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u043b\u0443\u0433\u0430\u0442\u0430 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0430", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "API \u043a\u043b\u044e\u0447" + }, + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u043d\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430" + }, + "user": { + "data": { + "api_key": "API \u043a\u043b\u044e\u0447", + "url": "URL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lidarr/translations/ca.json b/homeassistant/components/lidarr/translations/ca.json new file mode 100644 index 00000000000..78d0904b50a --- /dev/null +++ b/homeassistant/components/lidarr/translations/ca.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "already_configured": "El servei 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", + "unknown": "Error inesperat", + "wrong_app": "No s'ha trobat l'aplicaci\u00f3 correcta. Torna-ho a intentar", + "zeroconf_failed": "No s'ha trobat la clau API. Introdueix-la manualment" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "Clau API" + }, + "description": "La integraci\u00f3 Lidarr ha de tornar a autenticar-se manualment amb l'API de Lidarr", + "title": "Reautenticaci\u00f3 de la integraci\u00f3" + }, + "user": { + "data": { + "api_key": "Clau API", + "url": "URL", + "verify_ssl": "Verifica el certificat SSL" + }, + "description": "La clau API es pot recuperar autom\u00e0ticament si les credencials d'inici de sessi\u00f3 no s'han establert a l'aplicaci\u00f3.\nLa teva clau API es pot trobar a Configuraci\u00f3 ('Settings') > General, a la interf\u00edcie web de Lidarr." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "upcoming_days": "Nombre dies propers a mostrar al calendari" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lidarr/translations/de.json b/homeassistant/components/lidarr/translations/de.json new file mode 100644 index 00000000000..a51b9c24a2f --- /dev/null +++ b/homeassistant/components/lidarr/translations/de.json @@ -0,0 +1,42 @@ +{ + "config": { + "abort": { + "already_configured": "Der Dienst ist bereits konfiguriert", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "unknown": "Unerwarteter Fehler", + "wrong_app": "Falsche Anwendung erreicht. Bitte versuche es erneut", + "zeroconf_failed": "API-Schl\u00fcssel nicht gefunden. Bitte gib ihn manuell ein" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "API-Schl\u00fcssel" + }, + "description": "Die Lidarr-Integration muss manuell erneut mit der Lidarr-API authentifiziert werden", + "title": "Integration erneut authentifizieren" + }, + "user": { + "data": { + "api_key": "API-Schl\u00fcssel", + "url": "URL", + "verify_ssl": "SSL-Zertifikat \u00fcberpr\u00fcfen" + }, + "description": "Der API-Schl\u00fcssel kann automatisch abgerufen werden, wenn in der Anwendung keine Anmeldeinformationen festgelegt wurden.\nDeinen API-Schl\u00fcssel findest du unter Einstellungen > Allgemein in der Lidarr-Web-Benutzeroberfl\u00e4che." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "max_records": "Anzahl der maximal anzuzeigenden Datens\u00e4tze f\u00fcr Gesucht und Warteschlange", + "upcoming_days": "Anzahl der kommenden Tage, die im Kalender angezeigt werden sollen" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lidarr/translations/el.json b/homeassistant/components/lidarr/translations/el.json new file mode 100644 index 00000000000..01a54904034 --- /dev/null +++ b/homeassistant/components/lidarr/translations/el.json @@ -0,0 +1,42 @@ +{ + "config": { + "abort": { + "already_configured": "\u0397 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af", + "reauth_successful": "\u039f \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2" + }, + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1", + "wrong_app": "\u0395\u03c0\u03af\u03c4\u03b5\u03c5\u03be\u03b7 \u03bb\u03b1\u03bd\u03b8\u03b1\u03c3\u03bc\u03ad\u03bd\u03b7\u03c2 \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae\u03c2. \u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03ce \u03b4\u03bf\u03ba\u03b9\u03bc\u03ac\u03c3\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac", + "zeroconf_failed": "\u03a4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API \u03b4\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b5. \u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03ce \u03b5\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf \u03c7\u03b5\u03b9\u03c1\u03bf\u03ba\u03af\u03bd\u03b7\u03c4\u03b1" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API" + }, + "description": "\u0397 \u03b5\u03bd\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03c4\u03bf\u03c5 Lidarr \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 \u03bc\u03b5 \u03c4\u03bf Lidarr API", + "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", + "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" + }, + "description": "\u03a4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03b1\u03bd\u03b1\u03ba\u03c4\u03b7\u03b8\u03b5\u03af \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b1 \u03b5\u03ac\u03bd \u03c4\u03b1 \u03b4\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03ae\u03c1\u03b9\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03b4\u03b5\u03bd \u03ad\u03c7\u03bf\u03c5\u03bd \u03bf\u03c1\u03b9\u03c3\u03c4\u03b5\u03af \u03c3\u03c4\u03b7\u03bd \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae.\n\u03a4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03b2\u03c1\u03b5\u03b8\u03b5\u03af \u03c3\u03c4\u03b9\u03c2 \u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 > \u0393\u03b5\u03bd\u03b9\u03ba\u03ac \u03c3\u03c4\u03bf Lidarr Web UI." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "max_records": "\u0391\u03c1\u03b9\u03b8\u03bc\u03cc\u03c2 \u03bc\u03ad\u03b3\u03b9\u03c3\u03c4\u03c9\u03bd \u03b5\u03b3\u03b3\u03c1\u03b1\u03c6\u03ce\u03bd \u03b3\u03b9\u03b1 \u03b5\u03bc\u03c6\u03ac\u03bd\u03b9\u03c3\u03b7 \u03c3\u03c4\u03b7\u03bd \u03b5\u03c0\u03b9\u03b8\u03c5\u03bc\u03b7\u03c4\u03ae \u03ba\u03b1\u03b9 \u03c3\u03c4\u03b7\u03bd \u03bf\u03c5\u03c1\u03ac", + "upcoming_days": "\u0391\u03c1\u03b9\u03b8\u03bc\u03cc\u03c2 \u03b5\u03c0\u03b5\u03c1\u03c7\u03cc\u03bc\u03b5\u03bd\u03c9\u03bd \u03b7\u03bc\u03b5\u03c1\u03ce\u03bd \u03c0\u03bf\u03c5 \u03b8\u03b1 \u03b5\u03bc\u03c6\u03b1\u03bd\u03af\u03b6\u03bf\u03bd\u03c4\u03b1\u03b9 \u03c3\u03c4\u03bf \u03b7\u03bc\u03b5\u03c1\u03bf\u03bb\u03cc\u03b3\u03b9\u03bf" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lidarr/translations/en.json b/homeassistant/components/lidarr/translations/en.json new file mode 100644 index 00000000000..0e0475d25cd --- /dev/null +++ b/homeassistant/components/lidarr/translations/en.json @@ -0,0 +1,42 @@ +{ + "config": { + "abort": { + "already_configured": "Service is already configured", + "reauth_successful": "Re-authentication was successful" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error", + "wrong_app": "Incorrect application reached. Please try again", + "zeroconf_failed": "API key not found. Please enter it manually" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "API Key" + }, + "description": "The Lidarr integration needs to be manually re-authenticated with the Lidarr API", + "title": "Reauthenticate Integration" + }, + "user": { + "data": { + "api_key": "API Key", + "url": "URL", + "verify_ssl": "Verify SSL certificate" + }, + "description": "API key can be retrieved automatically if login credentials were not set in application.\nYour API key can be found in Settings > General in the Lidarr Web UI." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "max_records": "Number of maximum records to display on wanted and queue", + "upcoming_days": "Number of upcoming days to display on calendar" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lidarr/translations/es.json b/homeassistant/components/lidarr/translations/es.json new file mode 100644 index 00000000000..071ee1312ec --- /dev/null +++ b/homeassistant/components/lidarr/translations/es.json @@ -0,0 +1,42 @@ +{ + "config": { + "abort": { + "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", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "unknown": "Error inesperado", + "wrong_app": "Se ha alcanzado una aplicaci\u00f3n incorrecta. Por favor, int\u00e9ntalo de nuevo", + "zeroconf_failed": "Clave API no encontrada. Por favor, introd\u00facela manualmente" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "Clave API" + }, + "description": "La integraci\u00f3n Lidarr debe volver a autenticarse manualmente con la API de Lidarr", + "title": "Volver a autenticar la integraci\u00f3n" + }, + "user": { + "data": { + "api_key": "Clave API", + "url": "URL", + "verify_ssl": "Verificar el certificado SSL" + }, + "description": "La clave API se puede recuperar autom\u00e1ticamente si las credenciales de inicio de sesi\u00f3n no se configuraron en la aplicaci\u00f3n.\nTu clave API se puede encontrar en Configuraci\u00f3n > General en la IU web de Lidarr." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "max_records": "N\u00famero m\u00e1ximo de registros para mostrar en b\u00fasqueda y cola", + "upcoming_days": "N\u00famero de pr\u00f3ximos d\u00edas para mostrar en el calendario" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lidarr/translations/fr.json b/homeassistant/components/lidarr/translations/fr.json new file mode 100644 index 00000000000..9eb6bf92cd2 --- /dev/null +++ b/homeassistant/components/lidarr/translations/fr.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "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", + "invalid_auth": "Authentification non valide", + "unknown": "Erreur inattendue", + "wrong_app": "Une application incorrecte a \u00e9t\u00e9 atteinte. Veuillez r\u00e9essayer", + "zeroconf_failed": "La cl\u00e9 d'API n'a pas \u00e9t\u00e9 trouv\u00e9e. Veuillez la saisir manuellement" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "Cl\u00e9 d'API" + }, + "description": "L'int\u00e9gration Lidarr doit \u00eatre r\u00e9-authentifi\u00e9e manuellement avec l'API Lidarr", + "title": "R\u00e9-authentifier l'int\u00e9gration" + }, + "user": { + "data": { + "api_key": "Cl\u00e9 d'API", + "url": "URL", + "verify_ssl": "V\u00e9rifier le certificat SSL" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "max_records": "Nombre maximal d'enregistrements \u00e0 afficher sur la recherche et la file d'attente", + "upcoming_days": "Nombre de jours \u00e0 venir \u00e0 afficher sur le calendrier" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lidarr/translations/he.json b/homeassistant/components/lidarr/translations/he.json new file mode 100644 index 00000000000..7f63149230a --- /dev/null +++ b/homeassistant/components/lidarr/translations/he.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "\u05e9\u05d9\u05e8\u05d5\u05ea \u05d6\u05d4 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8", + "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "\u05de\u05e4\u05ea\u05d7 API" + }, + "title": "\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05e9\u05dc \u05e9\u05d9\u05dc\u05d5\u05d1" + }, + "user": { + "data": { + "api_key": "\u05de\u05e4\u05ea\u05d7 API", + "url": "\u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8", + "verify_ssl": "\u05d0\u05d9\u05de\u05d5\u05ea \u05d0\u05d9\u05e9\u05d5\u05e8 SSL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lidarr/translations/hu.json b/homeassistant/components/lidarr/translations/hu.json new file mode 100644 index 00000000000..a47d23df43c --- /dev/null +++ b/homeassistant/components/lidarr/translations/hu.json @@ -0,0 +1,42 @@ +{ + "config": { + "abort": { + "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", + "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", + "zeroconf_failed": "Az API-kulcs nem tal\u00e1lhat\u00f3. K\u00e9rem, adja meg" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "API kulcs" + }, + "description": "A Lidarr integr\u00e1ci\u00f3t manu\u00e1lisan \u00fajra kell hiteles\u00edteni a Lidarr API-val.", + "title": "Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se" + }, + "user": { + "data": { + "api_key": "API kulcs", + "url": "URL", + "verify_ssl": "SSL-tan\u00fas\u00edtv\u00e1ny ellen\u0151rz\u00e9se" + }, + "description": "Az API-kulcs automatikusan lek\u00e9rhet\u0151, ha a bejelentkez\u00e9si hiteles\u00edt\u0151 adatok nem lettek be\u00e1ll\u00edtva az alkalmaz\u00e1sban.\nAz API-kulcs a Lidarr webes felhaszn\u00e1l\u00f3i fel\u00fclet Be\u00e1ll\u00edt\u00e1sok > \u00c1ltal\u00e1nos men\u00fcpontj\u00e1ban tal\u00e1lhat\u00f3." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "max_records": "A keresett \u00e9s a v\u00e1r\u00f3list\u00e1n megjelen\u00edtend\u0151 maxim\u00e1lis rekordok sz\u00e1ma", + "upcoming_days": "A napt\u00e1rban megjelen\u00edtend\u0151 k\u00f6vetkez\u0151 napok sz\u00e1ma" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lidarr/translations/id.json b/homeassistant/components/lidarr/translations/id.json new file mode 100644 index 00000000000..1514a016dc1 --- /dev/null +++ b/homeassistant/components/lidarr/translations/id.json @@ -0,0 +1,42 @@ +{ + "config": { + "abort": { + "already_configured": "Layanan sudah dikonfigurasi", + "reauth_successful": "Autentikasi ulang berhasil" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid", + "unknown": "Kesalahan yang tidak diharapkan", + "wrong_app": "Aplikasi yang salah tercapai. Silakan coba lagi", + "zeroconf_failed": "Kunci API tidak ditemukan. Silakan masukkan secara manual" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "Kunci API" + }, + "description": "Integrasi Lidarr perlu diautentikasi ulang secara manual dengan Lidarr API", + "title": "Autentikasi Ulang Integrasi" + }, + "user": { + "data": { + "api_key": "Kunci API", + "url": "URL", + "verify_ssl": "Verifikasi sertifikat SSL" + }, + "description": "Kunci API dapat diambil secara otomatis jika kredensial login tidak diatur dalam aplikasi.\nKunci API Anda dapat ditemukan di Settings > General di antarmuka web Lidarr." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "max_records": "Jumlah data maksimum untuk ditampilkan pada wanted dan queue", + "upcoming_days": "Jumlah hari yang akan datang untuk ditampilkan pada kalender" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lidarr/translations/it.json b/homeassistant/components/lidarr/translations/it.json new file mode 100644 index 00000000000..b040d3eeb00 --- /dev/null +++ b/homeassistant/components/lidarr/translations/it.json @@ -0,0 +1,42 @@ +{ + "config": { + "abort": { + "already_configured": "Il servizio \u00e8 gi\u00e0 configurato", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida", + "unknown": "Errore imprevisto", + "wrong_app": "Applicazione errata raggiunta. Per favore riprova", + "zeroconf_failed": "Chiave API non trovata. Si prega di inserirla manualmente" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "Chiave API" + }, + "description": "L'integrazione Lidarr deve essere riautenticata manualmente con l'API Lidarr", + "title": "Autentica nuovamente l'integrazione" + }, + "user": { + "data": { + "api_key": "Chiave API", + "url": "URL", + "verify_ssl": "Verifica il certificato SSL" + }, + "description": "La chiave API pu\u00f2 essere recuperata automaticamente se le credenziali di accesso non sono state impostate nell'applicazione.\nLa tua chiave API pu\u00f2 essere trovata in Impostazioni > Generali nell'interfaccia utente web di Lidarr." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "max_records": "Numero massimo di record da visualizzare su ricercato e coda", + "upcoming_days": "Numero di giorni successivi da visualizzare sul calendario" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lidarr/translations/nl.json b/homeassistant/components/lidarr/translations/nl.json new file mode 100644 index 00000000000..0ec6ffdb679 --- /dev/null +++ b/homeassistant/components/lidarr/translations/nl.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "Dienst 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": { + "api_key": "API-sleutel" + }, + "title": "Integratie herauthenticeren" + }, + "user": { + "data": { + "api_key": "API-sleutel", + "url": "URL", + "verify_ssl": "SSL-certificaat verifi\u00ebren" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lidarr/translations/no.json b/homeassistant/components/lidarr/translations/no.json new file mode 100644 index 00000000000..74514056485 --- /dev/null +++ b/homeassistant/components/lidarr/translations/no.json @@ -0,0 +1,42 @@ +{ + "config": { + "abort": { + "already_configured": "Tjenesten er allerede konfigurert", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning", + "unknown": "Uventet feil", + "wrong_app": "Feil s\u00f8knad er n\u00e5dd. V\u00e6r s\u00e5 snill, pr\u00f8v p\u00e5 nytt", + "zeroconf_failed": "Finner ikke API-n\u00f8kkel. Vennligst skriv det inn manuelt" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "API-n\u00f8kkel" + }, + "description": "Lidarr-integrasjonen m\u00e5 re-autentiseres manuelt med Lidarr API", + "title": "Godkjenne integrering p\u00e5 nytt" + }, + "user": { + "data": { + "api_key": "API-n\u00f8kkel", + "url": "URL", + "verify_ssl": "Verifisere SSL-sertifikat" + }, + "description": "API-n\u00f8kkel kan hentes automatisk hvis p\u00e5loggingsinformasjon ikke ble angitt i applikasjonen.\n API-n\u00f8kkelen din finner du i Innstillinger > Generelt i Lidarr Web UI." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "max_records": "Antall maksimale poster \u00e5 vise p\u00e5 \u00f8nsket og k\u00f8", + "upcoming_days": "Antall kommende dager som skal vises i kalenderen" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lidarr/translations/pl.json b/homeassistant/components/lidarr/translations/pl.json new file mode 100644 index 00000000000..33d0deee79b --- /dev/null +++ b/homeassistant/components/lidarr/translations/pl.json @@ -0,0 +1,42 @@ +{ + "config": { + "abort": { + "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", + "invalid_auth": "Niepoprawne uwierzytelnienie", + "unknown": "Nieoczekiwany b\u0142\u0105d", + "wrong_app": "Osi\u0105gni\u0119to nieprawid\u0142ow\u0105 aplikacj\u0119. Spr\u00f3buj ponownie", + "zeroconf_failed": "Nie znaleziono klucza API. Prosz\u0119 wpisa\u0107 go r\u0119cznie." + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "Klucz API" + }, + "description": "Integracja Lidarr musi zosta\u0107 r\u0119cznie ponownie uwierzytelniona za pomoc\u0105 interfejsu API Lidarr", + "title": "Ponownie uwierzytelnij integracj\u0119" + }, + "user": { + "data": { + "api_key": "Klucz API", + "url": "URL", + "verify_ssl": "Weryfikacja certyfikatu SSL" + }, + "description": "Klucz API mo\u017ce zosta\u0107 pobrany automatycznie, je\u015bli dane logowania nie zosta\u0142y ustawione w aplikacji.\nTw\u00f3j klucz API mo\u017cesz znale\u017a\u0107 w Ustawienia > Og\u00f3lne, na swoim koncie Lidarr." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "max_records": "Maksymalna liczba wpis\u00f3w do wy\u015bwietlenia w poszukiwanych i w kolejce", + "upcoming_days": "Liczba nadchodz\u0105cych dni do wy\u015bwietlenia w kalendarzu" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lidarr/translations/pt-BR.json b/homeassistant/components/lidarr/translations/pt-BR.json new file mode 100644 index 00000000000..9390e86b497 --- /dev/null +++ b/homeassistant/components/lidarr/translations/pt-BR.json @@ -0,0 +1,42 @@ +{ + "config": { + "abort": { + "already_configured": "O servi\u00e7o 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", + "unknown": "Erro inesperado", + "wrong_app": "Aplica\u00e7\u00e3o incorreta alcan\u00e7ada. Por favor, tente novamente", + "zeroconf_failed": "Chave de API n\u00e3o encontrada. Por favor, insira manualmente" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "Chave de API" + }, + "description": "A integra\u00e7\u00e3o do Lidarr precisa ser autenticada manualmente com a API do Lidarr", + "title": "Reautenticar Integra\u00e7\u00e3o" + }, + "user": { + "data": { + "api_key": "Chave de API", + "url": "URL", + "verify_ssl": "Verificar certificado SSL" + }, + "description": "A chave de API pode ser recuperada automaticamente se as credenciais de login n\u00e3o tiverem sido definidas no aplicativo.\n Sua chave de API pode ser encontrada em Configura\u00e7\u00f5es > Geral na IU da Web do Lidarr." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "max_records": "N\u00famero m\u00e1ximo de registros a serem exibidos em desejados e em fila", + "upcoming_days": "N\u00famero de pr\u00f3ximos dias a serem exibidos no calend\u00e1rio" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lidarr/translations/ru.json b/homeassistant/components/lidarr/translations/ru.json new file mode 100644 index 00000000000..afda2835228 --- /dev/null +++ b/homeassistant/components/lidarr/translations/ru.json @@ -0,0 +1,42 @@ +{ + "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": { + "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.", + "wrong_app": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435. \u041f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0451 \u0440\u0430\u0437.", + "zeroconf_failed": "\u041a\u043b\u044e\u0447 API \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u0435\u0433\u043e \u0432\u0440\u0443\u0447\u043d\u0443\u044e." + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API" + }, + "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\u0438 \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e API lidarr", + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" + }, + "user": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API", + "url": "URL-\u0430\u0434\u0440\u0435\u0441", + "verify_ssl": "\u041f\u0440\u043e\u0432\u0435\u0440\u044f\u0442\u044c \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL" + }, + "description": "\u041a\u043b\u044e\u0447 API \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u043f\u043e\u043b\u0443\u0447\u0435\u043d \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438, \u0435\u0441\u043b\u0438 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0434\u043b\u044f \u0432\u0445\u043e\u0434\u0430 \u043d\u0435 \u0431\u044b\u043b\u0438 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u044b \u0432 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0438.\n\u0412\u0430\u0448 \u043a\u043b\u044e\u0447 API \u043c\u043e\u0436\u043d\u043e \u043d\u0430\u0439\u0442\u0438 \u0432 \u0440\u0430\u0437\u0434\u0435\u043b\u0435 \u00ab\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 > \u00ab\u041e\u0441\u043d\u043e\u0432\u043d\u044b\u0435\u00bb \u0432 \u0432\u0435\u0431-\u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0435 Lidarr." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "max_records": "\u041c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u043e\u0435 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u0437\u0430\u043f\u0438\u0441\u0435\u0439 \u0434\u043b\u044f \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f \u0432 \u043f\u043e\u0438\u0441\u043a\u0435 \u0438 \u0432 \u043e\u0447\u0435\u0440\u0435\u0434\u0438", + "upcoming_days": "\u041a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u043f\u0440\u0435\u0434\u0441\u0442\u043e\u044f\u0449\u0438\u0445 \u0434\u043d\u0435\u0439 \u0434\u043b\u044f \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f \u0432 \u043a\u0430\u043b\u0435\u043d\u0434\u0430\u0440\u0435" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lidarr/translations/zh-Hant.json b/homeassistant/components/lidarr/translations/zh-Hant.json new file mode 100644 index 00000000000..d4d5b860b20 --- /dev/null +++ b/homeassistant/components/lidarr/translations/zh-Hant.json @@ -0,0 +1,42 @@ +{ + "config": { + "abort": { + "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", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4", + "wrong_app": "\u5b58\u53d6\u61c9\u7528\u7a0b\u5f0f\u4e0d\u6b63\u78ba\uff0c\u8acb\u518d\u8a66\u4e00\u6b21", + "zeroconf_failed": "\u627e\u4e0d\u5230 API \u91d1\u9470\u3001\u8acb\u624b\u52d5\u8f38\u5165\u3002" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "API \u91d1\u9470" + }, + "description": "Lidarr \u6574\u5408\u9700\u8981\u624b\u52d5\u91cd\u65b0\u8a8d\u8b49 Lidarr API", + "title": "\u91cd\u65b0\u8a8d\u8b49\u6574\u5408" + }, + "user": { + "data": { + "api_key": "API \u91d1\u9470", + "url": "\u7db2\u5740", + "verify_ssl": "\u78ba\u8a8d SSL \u8a8d\u8b49" + }, + "description": "\u5047\u5982\u6c92\u6709\u65bc\u61c9\u7528\u7a0b\u5f0f\u4e2d\u8a2d\u5b9a\u767b\u5165\u6191\u8b49\uff0c\u5247\u53ef\u4ee5\u81ea\u52d5\u53d6\u5f97 API \u91d1\u9470\u3002\n\u91d1\u9470\u53ef\u4ee5\u65bc Lidarr Web \u4ecb\u9762\u4e2d\u8a2d\u5b9a\uff08Settings\uff09 > \u4e00\u822c\uff08General\uff09\u4e2d\u53d6\u5f97\u3002" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "max_records": "\u986f\u793a\u60f3\u8981\u8207\u6392\u968a\u6700\u9ad8\u7d00\u9304\u6578\u76ee", + "upcoming_days": "\u5373\u5c07\u5230\u4f86\u884c\u4e8b\u66c6\u986f\u793a\u5929\u6578" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/life360/config_flow.py b/homeassistant/components/life360/config_flow.py index 331882aa991..5153e389d8b 100644 --- a/homeassistant/components/life360/config_flow.py +++ b/homeassistant/components/life360/config_flow.py @@ -12,10 +12,10 @@ from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from .const import ( - COMM_MAX_RETRIES, COMM_TIMEOUT, CONF_AUTHORIZATION, CONF_DRIVING_SPEED, @@ -53,13 +53,10 @@ class Life360ConfigFlow(ConfigFlow, domain=DOMAIN): """Life360 integration config flow.""" VERSION = 1 - - def __init__(self) -> None: - """Initialize.""" - self._api = Life360(timeout=COMM_TIMEOUT, max_retries=COMM_MAX_RETRIES) - self._username: str | vol.UNDEFINED = vol.UNDEFINED - self._password: str | vol.UNDEFINED = vol.UNDEFINED - self._reauth_entry: ConfigEntry | None = None + _api: Life360 | None = None + _username: str | vol.UNDEFINED = vol.UNDEFINED + _password: str | vol.UNDEFINED = vol.UNDEFINED + _reauth_entry: ConfigEntry | None = None @staticmethod @callback @@ -69,10 +66,14 @@ class Life360ConfigFlow(ConfigFlow, domain=DOMAIN): async def _async_verify(self, step_id: str) -> FlowResult: """Attempt to authorize the provided credentials.""" + if not self._api: + self._api = Life360( + session=async_get_clientsession(self.hass), timeout=COMM_TIMEOUT + ) errors: dict[str, str] = {} try: - authorization = await self.hass.async_add_executor_job( - self._api.get_authorization, self._username, self._password + authorization = await self._api.get_authorization( + self._username, self._password ) except LoginError as exc: LOGGER.debug("Login error: %s", exc) diff --git a/homeassistant/components/life360/const.py b/homeassistant/components/life360/const.py index ccaf69877d6..21bf9a89c5e 100644 --- a/homeassistant/components/life360/const.py +++ b/homeassistant/components/life360/const.py @@ -7,7 +7,6 @@ DOMAIN = "life360" LOGGER = logging.getLogger(__package__) ATTRIBUTION = "Data provided by life360.com" -COMM_MAX_RETRIES = 2 COMM_TIMEOUT = 3.05 SPEED_FACTOR_MPH = 2.25 SPEED_DIGITS = 1 diff --git a/homeassistant/components/life360/coordinator.py b/homeassistant/components/life360/coordinator.py index ed774bba8ca..edb86e9727a 100644 --- a/homeassistant/components/life360/coordinator.py +++ b/homeassistant/components/life360/coordinator.py @@ -19,12 +19,12 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed +from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from homeassistant.util.distance import convert -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util +from homeassistant.util.unit_conversion import DistanceConverter from .const import ( - COMM_MAX_RETRIES, COMM_TIMEOUT, CONF_AUTHORIZATION, DOMAIN, @@ -64,10 +64,7 @@ class Life360Circle: class Life360Member: """Life360 Member data.""" - # Don't include address field in eq comparison because it often changes (back and - # forth) between updates. If it was included there would be way more state changes - # and database updates than is useful. - address: str | None = field(compare=False) + address: str | None at_loc_since: datetime battery_charging: bool battery_level: int @@ -106,8 +103,8 @@ class Life360DataUpdateCoordinator(DataUpdateCoordinator[Life360Data]): ) self._hass = hass self._api = Life360( + session=async_get_clientsession(hass), timeout=COMM_TIMEOUT, - max_retries=COMM_MAX_RETRIES, authorization=entry.data[CONF_AUTHORIZATION], ) self._missing_loc_reason = hass.data[DOMAIN].missing_loc_reason @@ -115,9 +112,7 @@ class Life360DataUpdateCoordinator(DataUpdateCoordinator[Life360Data]): async def _retrieve_data(self, func: str, *args: Any) -> list[dict[str, Any]]: """Get data from Life360.""" try: - return await self._hass.async_add_executor_job( - getattr(self._api, func), *args - ) + return await getattr(self._api, func)(*args) except LoginError as exc: LOGGER.debug("Login error: %s", exc) raise ConfigEntryAuthFailed from exc @@ -203,19 +198,18 @@ class Life360DataUpdateCoordinator(DataUpdateCoordinator[Life360Data]): place = loc["name"] or None - if place: - address: str | None = place + address1: str | None = loc["address1"] or None + address2: str | None = loc["address2"] or None + if address1 and address2: + address: str | None = ", ".join([address1, address2]) else: - address1 = loc["address1"] or None - address2 = loc["address2"] or None - if address1 and address2: - address = ", ".join([address1, address2]) - else: - address = address1 or address2 + address = address1 or address2 speed = max(0, float(loc["speed"]) * SPEED_FACTOR_MPH) if self._hass.config.units.is_metric: - speed = convert(speed, LENGTH_MILES, LENGTH_KILOMETERS) + speed = DistanceConverter.convert( + speed, LENGTH_MILES, LENGTH_KILOMETERS + ) data.members[member_id] = Life360Member( address, @@ -226,7 +220,11 @@ class Life360DataUpdateCoordinator(DataUpdateCoordinator[Life360Data]): member["avatar"], # Life360 reports accuracy in feet, but Device Tracker expects # gps_accuracy in meters. - round(convert(float(loc["accuracy"]), LENGTH_FEET, LENGTH_METERS)), + round( + DistanceConverter.convert( + float(loc["accuracy"]), LENGTH_FEET, LENGTH_METERS + ) + ), dt_util.utc_from_timestamp(int(loc["timestamp"])), float(loc["latitude"]), float(loc["longitude"]), diff --git a/homeassistant/components/life360/device_tracker.py b/homeassistant/components/life360/device_tracker.py index 1fa63a7659a..2c05b944a27 100644 --- a/homeassistant/components/life360/device_tracker.py +++ b/homeassistant/components/life360/device_tracker.py @@ -3,6 +3,7 @@ from __future__ import annotations from collections.abc import Mapping +from contextlib import suppress from typing import Any, cast from homeassistant.components.device_tracker import SourceType @@ -114,6 +115,17 @@ class Life360DeviceTracker( self._attr_name = self._data.name self._attr_entity_picture = self._data.entity_picture + # Server sends a pair of address values on alternate updates. Keep the pair of + # values so they can be combined into the one address attribute. + # The pair will either be two different address values, or one address and a + # copy of the Place value (if the Member is in a Place.) In the latter case we + # won't duplicate the Place name, but rather just use one the address value. Use + # the value of None to hold one of the "slots" in the list so we'll know not to + # expect another address value. + if (address := self._data.address) == self._data.place: + address = None + self._addresses = [address] + @property def _options(self) -> Mapping[str, Any]: """Shortcut to config entry options.""" @@ -160,13 +172,41 @@ class Life360DeviceTracker( for attr in _LOC_ATTRS: setattr(self._data, attr, getattr(self._prev_data, attr)) + else: + # Process address field. + # Check if we got the name of a Place, which we won't use. + if (address := self._data.address) == self._data.place: + address = None + if last_seen != prev_seen: + # We have new location data, so we might have a new pair of address + # values. + if address not in self._addresses: + # We do. + # Replace the old values with the first value of the new pair. + self._addresses = [address] + elif self._data.address != self._prev_data.address: + # Location data didn't change in general, but the address field did. + # There are three possibilities: + # 1. The new value is one of the pair we've already seen before. + # 2. The new value is the second of the pair we haven't seen yet. + # 3. The new value is the first of a new pair of values. + if address not in self._addresses: + if len(self._addresses) < 2: + self._addresses.append(address) + else: + self._addresses = [address] + self._prev_data = self._data super()._handle_coordinator_update() @property def force_update(self) -> bool: - """Return True if state updates should be forced.""" + """Return True if state updates should be forced. + + Overridden because CoordinatorEntity sets `should_poll` to False, + which causes TrackerEntity to set `force_update` to True. + """ return False @property @@ -246,8 +286,26 @@ class Life360DeviceTracker( ATTR_SPEED: None, ATTR_WIFI_ON: None, } + + # Generate address attribute from pair of address values. + # There may be two, one or no values. If there are two, sort the strings since + # one value is typically a numbered street address and the other is a street, + # town or state name, and it's helpful to start with the more detailed address + # value. Also, sorting helps to generate the same result if we get a location + # update, and the same pair is sent afterwards, but where the value that comes + # first is swapped vs the order they came in before the update. + address1: str | None = None + address2: str | None = None + with suppress(IndexError): + address1 = self._addresses[0] + address2 = self._addresses[1] + if address1 and address2: + address: str | None = " / ".join(sorted([address1, address2])) + else: + address = address1 or address2 + return { - ATTR_ADDRESS: self._data.address, + ATTR_ADDRESS: address, ATTR_AT_LOC_SINCE: self._data.at_loc_since, ATTR_BATTERY_CHARGING: self._data.battery_charging, ATTR_DRIVING: self.driving, diff --git a/homeassistant/components/life360/manifest.json b/homeassistant/components/life360/manifest.json index 23fdad892d2..8f0c44f342b 100644 --- a/homeassistant/components/life360/manifest.json +++ b/homeassistant/components/life360/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/life360", "codeowners": ["@pnbruckner"], - "requirements": ["life360==4.1.1"], + "requirements": ["life360==5.1.1"], "iot_class": "cloud_polling", "loggers": ["life360"] } diff --git a/homeassistant/components/life360/translations/bg.json b/homeassistant/components/life360/translations/bg.json index d206a606b89..22fad245c5d 100644 --- a/homeassistant/components/life360/translations/bg.json +++ b/homeassistant/components/life360/translations/bg.json @@ -1,17 +1,26 @@ { "config": { "abort": { - "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" + "already_configured": "\u0410\u043a\u0430\u0443\u043d\u0442\u044a\u0442 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d", + "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" }, "create_entry": { "default": "\u0417\u0430 \u0434\u0430 \u0437\u0430\u0434\u0430\u0434\u0435\u0442\u0435 \u0440\u0430\u0437\u0448\u0438\u0440\u0435\u043d\u0438 \u043e\u043f\u0446\u0438\u0438, \u0432\u0438\u0436\u0442\u0435 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044f \u043d\u0430 Life360]({docs_url})." }, "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435", "invalid_username": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435", "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "step": { + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430" + }, + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u043d\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430" + }, "user": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u0430", @@ -21,5 +30,16 @@ "title": "\u0418\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f \u0437\u0430 Life360 \u043f\u0440\u043e\u0444\u0438\u043b" } } + }, + "options": { + "step": { + "init": { + "data": { + "driving_speed": "\u0421\u043a\u043e\u0440\u043e\u0441\u0442 \u043d\u0430 \u0448\u043e\u0444\u0438\u0440\u0430\u043d\u0435", + "max_gps_accuracy": "\u041c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u043d\u0430 GPS \u0442\u043e\u0447\u043d\u043e\u0441\u0442 (\u043c\u0435\u0442\u0440\u0438)" + }, + "title": "\u041e\u043f\u0446\u0438\u0438 \u043d\u0430 \u0430\u043a\u0430\u0443\u043d\u0442\u0430" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/life360/translations/cs.json b/homeassistant/components/life360/translations/cs.json index 0c267ef7163..89e4299178d 100644 --- a/homeassistant/components/life360/translations/cs.json +++ b/homeassistant/components/life360/translations/cs.json @@ -1,7 +1,9 @@ { "config": { "abort": { + "already_configured": "\u00da\u010det je ji\u017e nastaven", "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, "create_entry": { @@ -9,11 +11,18 @@ }, "error": { "already_configured": "\u00da\u010det je ji\u017e nastaven", + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", "invalid_username": "Neplatn\u00e9 u\u017eivatelsk\u00e9 jm\u00e9no", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, "step": { + "reauth_confirm": { + "data": { + "password": "Heslo" + }, + "title": "Znovu ov\u011b\u0159it integraci" + }, "user": { "data": { "password": "Heslo", diff --git a/homeassistant/components/life360/translations/es.json b/homeassistant/components/life360/translations/es.json index 72e880463fe..a9b53fffd86 100644 --- a/homeassistant/components/life360/translations/es.json +++ b/homeassistant/components/life360/translations/es.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente", "unknown": "Error inesperado" }, "create_entry": { diff --git a/homeassistant/components/lifx/__init__.py b/homeassistant/components/lifx/__init__.py index 5c91efa1d02..2f20cb0e366 100644 --- a/homeassistant/components/lifx/__init__.py +++ b/homeassistant/components/lifx/__init__.py @@ -57,7 +57,7 @@ CONFIG_SCHEMA = vol.All( ) -PLATFORMS = [Platform.BINARY_SENSOR, Platform.BUTTON, Platform.LIGHT] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.BUTTON, Platform.LIGHT, Platform.SELECT] DISCOVERY_INTERVAL = timedelta(minutes=15) MIGRATION_INTERVAL = timedelta(minutes=5) diff --git a/homeassistant/components/lifx/const.py b/homeassistant/components/lifx/const.py index 74960d59bd1..8acfa35802e 100644 --- a/homeassistant/components/lifx/const.py +++ b/homeassistant/components/lifx/const.py @@ -37,7 +37,13 @@ ATTR_REMAINING = "remaining" ATTR_ZONES = "zones" HEV_CYCLE_STATE = "hev_cycle_state" - +INFRARED_BRIGHTNESS = "infrared_brightness" +INFRARED_BRIGHTNESS_VALUES_MAP = { + 0: "Disabled", + 16383: "25%", + 32767: "50%", + 65535: "100%", +} DATA_LIFX_MANAGER = "lifx_manager" _LOGGER = logging.getLogger(__package__) diff --git a/homeassistant/components/lifx/coordinator.py b/homeassistant/components/lifx/coordinator.py index 37e753c27a3..a6d61d91d28 100644 --- a/homeassistant/components/lifx/coordinator.py +++ b/homeassistant/components/lifx/coordinator.py @@ -3,31 +3,50 @@ from __future__ import annotations import asyncio from datetime import timedelta +from enum import IntEnum from functools import partial from typing import Any, cast -from aiolifx.aiolifx import Light +from aiolifx.aiolifx import Light, MultiZoneDirection, MultiZoneEffectType from aiolifx.connection import LIFXConnection +from homeassistant.const import Platform from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import ( _LOGGER, ATTR_REMAINING, + DOMAIN, IDENTIFY_WAVEFORM, MESSAGE_RETRIES, MESSAGE_TIMEOUT, TARGET_ANY, UNAVAILABLE_GRACE, ) -from .util import async_execute_lifx, get_real_mac_addr, lifx_features +from .util import ( + async_execute_lifx, + get_real_mac_addr, + infrared_brightness_option_to_value, + infrared_brightness_value_to_option, + lifx_features, +) REQUEST_REFRESH_DELAY = 0.35 LIFX_IDENTIFY_DELAY = 3.0 +class FirmwareEffect(IntEnum): + """Enumeration of LIFX firmware effects.""" + + OFF = 0 + MOVE = 1 + MORPH = 2 + FLAME = 3 + + class LIFXUpdateCoordinator(DataUpdateCoordinator): """DataUpdateCoordinator to gather data for a specific lifx device.""" @@ -42,7 +61,9 @@ class LIFXUpdateCoordinator(DataUpdateCoordinator): self.connection = connection self.device: Light = connection.device self.lock = asyncio.Lock() + self.active_effect = FirmwareEffect.OFF update_interval = timedelta(seconds=10) + super().__init__( hass, _LOGGER, @@ -83,6 +104,18 @@ class LIFXUpdateCoordinator(DataUpdateCoordinator): """Return the label of the bulb.""" return cast(str, self.device.label) + @property + def current_infrared_brightness(self) -> str | None: + """Return the current infrared brightness as a string.""" + return infrared_brightness_value_to_option(self.device.infrared_brightness) + + def async_get_entity_id(self, platform: Platform, key: str) -> str | None: + """Return the entity_id from the platform and key provided.""" + ent_reg = er.async_get(self.hass) + return ent_reg.async_get_entity_id( + platform, DOMAIN, f"{self.serial_number}_{key}" + ) + async def async_identify_bulb(self) -> None: """Identify the device by flashing it three times.""" bulb: Light = self.device @@ -103,6 +136,7 @@ class LIFXUpdateCoordinator(DataUpdateCoordinator): self.device.get_hostfirmware() if self.device.product is None: self.device.get_version() + response = await async_execute_lifx(self.device.get_color) if self.device.product is None: @@ -114,15 +148,17 @@ class LIFXUpdateCoordinator(DataUpdateCoordinator): if self.device.mac_addr == TARGET_ANY: self.device.mac_addr = response.target_addr + # Update model-specific configuration if lifx_features(self.device)["multizone"]: await self.async_update_color_zones() + await self.async_update_multizone_effect() if lifx_features(self.device)["hev"]: - if self.device.hev_cycle_configuration is None: - self.device.get_hev_configuration() - await self.async_get_hev_cycle() + if lifx_features(self.device)["infrared"]: + response = await async_execute_lifx(self.device.get_infrared) + async def async_update_color_zones(self) -> None: """Get updated color information for each zone.""" zone = 0 @@ -195,3 +231,42 @@ class LIFXUpdateCoordinator(DataUpdateCoordinator): apply=apply, ) ) + + async def async_update_multizone_effect(self) -> None: + """Update the device firmware effect running state.""" + await async_execute_lifx(self.device.get_multizone_effect) + self.active_effect = FirmwareEffect[self.device.effect.get("effect", "OFF")] + + async def async_set_multizone_effect( + self, effect: str, speed: float, direction: str, power_on: bool = True + ) -> None: + """Control the firmware-based Move effect on a multizone device.""" + if lifx_features(self.device)["multizone"] is True: + if power_on and self.device.power_level == 0: + await self.async_set_power(True, 0) + + await async_execute_lifx( + partial( + self.device.set_multizone_effect, + effect=MultiZoneEffectType[effect.upper()].value, + speed=speed, + direction=MultiZoneDirection[direction.upper()].value, + ) + ) + self.active_effect = FirmwareEffect[effect.upper()] + + def async_get_active_effect(self) -> int: + """Return the enum value of the currently active firmware effect.""" + return self.active_effect.value + + async def async_set_hev_cycle_state(self, enable: bool, duration: int = 0) -> None: + """Start or stop an HEV cycle on a LIFX Clean bulb.""" + if lifx_features(self.device)["hev"]: + await async_execute_lifx( + partial(self.device.set_hev_cycle, enable=enable, duration=duration) + ) + + async def async_set_infrared_brightness(self, option: str) -> None: + """Set infrared brightness.""" + infrared_brightness = infrared_brightness_option_to_value(option) + await async_execute_lifx(partial(self.device.set_infrared, infrared_brightness)) diff --git a/homeassistant/components/lifx/light.py b/homeassistant/components/lifx/light.py index 4237fca9be7..aa02e42a9bf 100644 --- a/homeassistant/components/lifx/light.py +++ b/homeassistant/components/lifx/light.py @@ -19,7 +19,7 @@ from homeassistant.components.light import ( LightEntityFeature, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.const import ATTR_ENTITY_ID, Platform from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_platform @@ -28,11 +28,21 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_point_in_utc_time import homeassistant.util.color as color_util -from .const import ATTR_INFRARED, ATTR_POWER, ATTR_ZONES, DATA_LIFX_MANAGER, DOMAIN -from .coordinator import LIFXUpdateCoordinator +from .const import ( + _LOGGER, + ATTR_DURATION, + ATTR_INFRARED, + ATTR_POWER, + ATTR_ZONES, + DATA_LIFX_MANAGER, + DOMAIN, + INFRARED_BRIGHTNESS, +) +from .coordinator import FirmwareEffect, LIFXUpdateCoordinator from .entity import LIFXEntity from .manager import ( SERVICE_EFFECT_COLORLOOP, + SERVICE_EFFECT_MOVE, SERVICE_EFFECT_PULSE, SERVICE_EFFECT_STOP, LIFXManager, @@ -43,14 +53,20 @@ LIFX_STATE_SETTLE_DELAY = 0.3 SERVICE_LIFX_SET_STATE = "set_state" -LIFX_SET_STATE_SCHEMA = cv.make_entity_service_schema( - { - **LIGHT_TURN_ON_SCHEMA, - ATTR_INFRARED: vol.All(vol.Coerce(int), vol.Clamp(min=0, max=255)), - ATTR_ZONES: vol.All(cv.ensure_list, [cv.positive_int]), - ATTR_POWER: cv.boolean, - } -) +LIFX_SET_STATE_SCHEMA = { + **LIGHT_TURN_ON_SCHEMA, + ATTR_INFRARED: vol.All(vol.Coerce(int), vol.Clamp(min=0, max=255)), + ATTR_ZONES: vol.All(cv.ensure_list, [cv.positive_int]), + ATTR_POWER: cv.boolean, +} + + +SERVICE_LIFX_SET_HEV_CYCLE_STATE = "set_hev_cycle_state" + +LIFX_SET_HEV_CYCLE_STATE_SCHEMA = { + ATTR_POWER: vol.Required(cv.boolean), + ATTR_DURATION: vol.All(vol.Coerce(float), vol.Clamp(min=0, max=86400)), +} HSBK_HUE = 0 HSBK_SATURATION = 1 @@ -74,6 +90,11 @@ async def async_setup_entry( LIFX_SET_STATE_SCHEMA, "set_state", ) + platform.async_register_entity_service( + SERVICE_LIFX_SET_HEV_CYCLE_STATE, + LIFX_SET_HEV_CYCLE_STATE_SCHEMA, + "set_hev_cycle_state", + ) if lifx_features(device)["multizone"]: entity: LIFXLight = LIFXStrip(coordinator, manager, entry) elif lifx_features(device)["color"]: @@ -119,6 +140,7 @@ class LIFXLight(LIFXEntity, LightEntity): color_mode = ColorMode.BRIGHTNESS self._attr_color_mode = color_mode self._attr_supported_color_modes = {color_mode} + self._attr_effect = None @property def brightness(self) -> int: @@ -143,6 +165,8 @@ class LIFXLight(LIFXEntity, LightEntity): """Return the name of the currently running effect.""" if effect := self.effects_conductor.effect(self.bulb): return f"effect_{effect.name}" + if effect := self.coordinator.async_get_active_effect(): + return f"effect_{FirmwareEffect(effect).name.lower()}" return None async def update_during_transition(self, when: int) -> None: @@ -194,6 +218,13 @@ class LIFXLight(LIFXEntity, LightEntity): return if ATTR_INFRARED in kwargs: + infrared_entity_id = self.coordinator.async_get_entity_id( + Platform.SELECT, INFRARED_BRIGHTNESS + ) + _LOGGER.warning( + "The 'infrared' attribute of 'lifx.set_state' is deprecated: call 'select.select_option' targeting '%s' instead", + infrared_entity_id, + ) bulb.set_infrared(convert_8_to_16(kwargs[ATTR_INFRARED])) if ATTR_TRANSITION in kwargs: @@ -232,6 +263,18 @@ class LIFXLight(LIFXEntity, LightEntity): # Update when the transition starts and ends await self.update_during_transition(fade) + async def set_hev_cycle_state( + self, power: bool, duration: int | None = None + ) -> None: + """Set the state of the HEV LEDs on a LIFX Clean bulb.""" + if lifx_features(self.bulb)["hev"] is False: + raise HomeAssistantError( + "This device does not support setting HEV cycle state" + ) + + await self.coordinator.async_set_hev_cycle_state(power, duration or 0) + await self.update_during_transition(duration or 0) + async def set_power( self, pwr: bool, @@ -322,6 +365,13 @@ class LIFXColor(LIFXLight): class LIFXStrip(LIFXColor): """Representation of a LIFX light strip with multiple zones.""" + _attr_effect_list = [ + SERVICE_EFFECT_COLORLOOP, + SERVICE_EFFECT_PULSE, + SERVICE_EFFECT_MOVE, + SERVICE_EFFECT_STOP, + ] + async def set_color( self, hsbk: list[float | int | None], diff --git a/homeassistant/components/lifx/manager.py b/homeassistant/components/lifx/manager.py index ee5428e36a8..c199ee8a9a1 100644 --- a/homeassistant/components/lifx/manager.py +++ b/homeassistant/components/lifx/manager.py @@ -1,6 +1,7 @@ """Support for LIFX lights.""" from __future__ import annotations +import asyncio from collections.abc import Callable from datetime import timedelta from typing import Any @@ -28,21 +29,35 @@ from homeassistant.core import HomeAssistant, ServiceCall, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.service import async_extract_referenced_entity_ids -from .const import _LOGGER, DATA_LIFX_MANAGER, DOMAIN +from .const import DATA_LIFX_MANAGER, DOMAIN +from .coordinator import LIFXUpdateCoordinator, Light from .util import convert_8_to_16, find_hsbk SCAN_INTERVAL = timedelta(seconds=10) - SERVICE_EFFECT_PULSE = "effect_pulse" SERVICE_EFFECT_COLORLOOP = "effect_colorloop" +SERVICE_EFFECT_MOVE = "effect_move" SERVICE_EFFECT_STOP = "effect_stop" +ATTR_POWER_OFF = "power_off" ATTR_POWER_ON = "power_on" ATTR_PERIOD = "period" ATTR_CYCLES = "cycles" ATTR_SPREAD = "spread" ATTR_CHANGE = "change" +ATTR_DIRECTION = "direction" +ATTR_SPEED = "speed" + +EFFECT_MOVE = "MOVE" +EFFECT_OFF = "OFF" + +EFFECT_MOVE_DEFAULT_SPEED = 3.0 +EFFECT_MOVE_DEFAULT_DIRECTION = "right" +EFFECT_MOVE_DIRECTION_RIGHT = "right" +EFFECT_MOVE_DIRECTION_LEFT = "left" + +EFFECT_MOVE_DIRECTIONS = [EFFECT_MOVE_DIRECTION_LEFT, EFFECT_MOVE_DIRECTION_RIGHT] PULSE_MODE_BLINK = "blink" PULSE_MODE_BREATHE = "breathe" @@ -110,10 +125,20 @@ LIFX_EFFECT_STOP_SCHEMA = cv.make_entity_service_schema({}) SERVICES = ( SERVICE_EFFECT_STOP, SERVICE_EFFECT_PULSE, + SERVICE_EFFECT_MOVE, SERVICE_EFFECT_COLORLOOP, ) +LIFX_EFFECT_MOVE_SCHEMA = cv.make_entity_service_schema( + { + **LIFX_EFFECT_SCHEMA, + ATTR_SPEED: vol.All(vol.Coerce(float), vol.Clamp(min=0.1, max=60)), + ATTR_DIRECTION: vol.In(EFFECT_MOVE_DIRECTIONS), + } +) + + class LIFXManager: """Representation of all known LIFX entities.""" @@ -168,6 +193,13 @@ class LIFXManager: schema=LIFX_EFFECT_COLORLOOP_SCHEMA, ) + self.hass.services.async_register( + DOMAIN, + SERVICE_EFFECT_MOVE, + service_handler, + schema=LIFX_EFFECT_MOVE_SCHEMA, + ) + self.hass.services.async_register( DOMAIN, SERVICE_EFFECT_STOP, @@ -179,15 +211,35 @@ class LIFXManager: self, entity_ids: set[str], service: str, **kwargs: Any ) -> None: """Start a light effect on entities.""" - bulbs = [ - coordinator.device - for entry_id, coordinator in self.hass.data[DOMAIN].items() - if entry_id != DATA_LIFX_MANAGER - and self.entry_id_to_entity_id[entry_id] in entity_ids - ] - _LOGGER.debug("Starting effect %s on %s", service, bulbs) - if service == SERVICE_EFFECT_PULSE: + coordinators: list[LIFXUpdateCoordinator] = [] + bulbs: list[Light] = [] + + for entry_id, coordinator in self.hass.data[DOMAIN].items(): + if ( + entry_id != DATA_LIFX_MANAGER + and self.entry_id_to_entity_id[entry_id] in entity_ids + ): + coordinators.append(coordinator) + bulbs.append(coordinator.device) + + if service == SERVICE_EFFECT_MOVE: + await asyncio.gather( + *( + coordinator.async_set_multizone_effect( + effect=EFFECT_MOVE, + speed=kwargs.get(ATTR_SPEED, EFFECT_MOVE_DEFAULT_SPEED), + direction=kwargs.get( + ATTR_DIRECTION, EFFECT_MOVE_DEFAULT_DIRECTION + ), + power_on=kwargs.get(ATTR_POWER_ON, False), + ) + for coordinator in coordinators + ) + ) + + elif service == SERVICE_EFFECT_PULSE: + effect = aiolifx_effects.EffectPulse( power_on=kwargs.get(ATTR_POWER_ON), period=kwargs.get(ATTR_PERIOD), @@ -196,8 +248,9 @@ class LIFXManager: hsbk=find_hsbk(self.hass, **kwargs), ) await self.effects_conductor.start(effect, bulbs) + elif service == SERVICE_EFFECT_COLORLOOP: - preprocess_turn_on_alternatives(self.hass, kwargs) # type: ignore[no-untyped-call] + preprocess_turn_on_alternatives(self.hass, kwargs) brightness = None if ATTR_BRIGHTNESS in kwargs: @@ -212,5 +265,15 @@ class LIFXManager: brightness=brightness, ) await self.effects_conductor.start(effect, bulbs) + elif service == SERVICE_EFFECT_STOP: + await self.effects_conductor.stop(bulbs) + + for coordinator in coordinators: + await coordinator.async_set_multizone_effect( + effect=EFFECT_OFF, + speed=EFFECT_MOVE_DEFAULT_SPEED, + direction=EFFECT_MOVE_DEFAULT_DIRECTION, + power_on=False, + ) diff --git a/homeassistant/components/lifx/manifest.json b/homeassistant/components/lifx/manifest.json index 83408f87bb5..45321f22b66 100644 --- a/homeassistant/components/lifx/manifest.json +++ b/homeassistant/components/lifx/manifest.json @@ -3,7 +3,7 @@ "name": "LIFX", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/lifx", - "requirements": ["aiolifx==0.8.2", "aiolifx_effects==0.2.2"], + "requirements": ["aiolifx==0.8.5", "aiolifx_effects==0.2.2"], "quality_scale": "platinum", "dependencies": ["network"], "homekit": { diff --git a/homeassistant/components/lifx/select.py b/homeassistant/components/lifx/select.py new file mode 100644 index 00000000000..a1cfb4624d5 --- /dev/null +++ b/homeassistant/components/lifx/select.py @@ -0,0 +1,69 @@ +"""Select sensor entities for LIFX integration.""" +from __future__ import annotations + +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, INFRARED_BRIGHTNESS, INFRARED_BRIGHTNESS_VALUES_MAP +from .coordinator import LIFXUpdateCoordinator +from .entity import LIFXEntity +from .util import lifx_features + +INFRARED_BRIGHTNESS_ENTITY = SelectEntityDescription( + key=INFRARED_BRIGHTNESS, + name="Infrared brightness", + entity_category=EntityCategory.CONFIG, +) + +INFRARED_BRIGHTNESS_OPTIONS = list(INFRARED_BRIGHTNESS_VALUES_MAP.values()) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up LIFX from a config entry.""" + coordinator: LIFXUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + + if lifx_features(coordinator.device)["infrared"]: + async_add_entities( + [ + LIFXInfraredBrightnessSelectEntity( + coordinator, description=INFRARED_BRIGHTNESS_ENTITY + ) + ] + ) + + +class LIFXInfraredBrightnessSelectEntity(LIFXEntity, SelectEntity): + """LIFX Nightvision infrared brightness configuration entity.""" + + _attr_has_entity_name = True + _attr_options = INFRARED_BRIGHTNESS_OPTIONS + + def __init__( + self, coordinator: LIFXUpdateCoordinator, description: SelectEntityDescription + ) -> None: + """Initialise the IR brightness config entity.""" + super().__init__(coordinator) + self.entity_description = description + self._attr_name = description.name + self._attr_unique_id = f"{coordinator.serial_number}_{description.key}" + self._attr_current_option = coordinator.current_infrared_brightness + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + self._async_update_attrs() + super()._handle_coordinator_update() + + @callback + def _async_update_attrs(self) -> None: + """Handle coordinator updates.""" + self._attr_current_option = self.coordinator.current_infrared_brightness + + async def async_select_option(self, option: str) -> None: + """Update the infrared brightness value.""" + await self.coordinator.async_set_infrared_brightness(option) diff --git a/homeassistant/components/lifx/services.yaml b/homeassistant/components/lifx/services.yaml index e499ad1b3b8..fc2e522dcd4 100644 --- a/homeassistant/components/lifx/services.yaml +++ b/homeassistant/components/lifx/services.yaml @@ -1,3 +1,29 @@ +set_hev_cycle_state: + name: Set HEV cycle state + description: Control the HEV LEDs on a LIFX Clean bulb. + target: + entity: + integration: lifx + domain: light + fields: + power: + name: enable + description: Start or stop a Clean cycle. + required: true + example: true + selector: + boolean: + duration: + name: Duration + description: How long the HEV LEDs will remain on. Uses the configured default duration if not specified. + required: false + default: 7200 + example: 3600 + selector: + number: + min: 0 + max: 86400 + unit_of_measurement: seconds set_state: name: Set State description: Set a color/brightness and possibly turn the light on/off. @@ -145,6 +171,40 @@ effect_colorloop: default: true selector: boolean: +effect_move: + name: Move effect + description: Start the firmware-based Move effect on a LIFX Z, Lightstrip or Beam. + target: + entity: + integration: lifx + domain: light + fields: + speed: + name: Speed + description: How long in seconds for the effect to move across the length of the light. + default: 3.0 + selector: + number: + min: 0.1 + max: 60 + step: 0.1 + unit_of_measurement: seconds + direction: + name: Direction + description: Direction the effect will move across the device. + default: right + selector: + select: + mode: dropdown + options: + - right + - left + power_on: + name: Power on + description: Powered off lights will be turned on before starting the effect. + default: true + selector: + boolean: effect_stop: name: Stop effect diff --git a/homeassistant/components/lifx/translations/bg.json b/homeassistant/components/lifx/translations/bg.json index e7ce46d836e..056e965f723 100644 --- a/homeassistant/components/lifx/translations/bg.json +++ b/homeassistant/components/lifx/translations/bg.json @@ -1,12 +1,30 @@ { "config": { "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", "no_devices_found": "\u0412 \u043c\u0440\u0435\u0436\u0430\u0442\u0430 \u043d\u044f\u043c\u0430 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 LIFX \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430.", "single_instance_allowed": "\u0412\u044a\u0437\u043c\u043e\u0436\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043d\u0430 LIFX." }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, + "flow_title": "{label} ({host}) {serial}", "step": { "confirm": { "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 LIFX?" + }, + "discovery_confirm": { + "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 {label} ({host}) {serial}?" + }, + "pick_device": { + "data": { + "device": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" + } + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + } } } } diff --git a/homeassistant/components/lifx/translations/cs.json b/homeassistant/components/lifx/translations/cs.json index 4b2e480e4bb..660884bc1e7 100644 --- a/homeassistant/components/lifx/translations/cs.json +++ b/homeassistant/components/lifx/translations/cs.json @@ -1,12 +1,25 @@ { "config": { "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "already_in_progress": "Konfigurace ji\u017e prob\u00edh\u00e1", "no_devices_found": "V s\u00edti nebyla nalezena \u017e\u00e1dn\u00e1 za\u0159\u00edzen\u00ed", "single_instance_allowed": "Ji\u017e nastaveno. Je mo\u017en\u00e1 pouze jedin\u00e1 konfigurace." }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" + }, "step": { "confirm": { "description": "Chcete nastavit LIFX?" + }, + "discovery_confirm": { + "description": "Chcete nastavit {label} ({host}) {serial}?" + }, + "user": { + "data": { + "host": "Hostitel" + } } } } diff --git a/homeassistant/components/lifx/util.py b/homeassistant/components/lifx/util.py index 1de8bdae76a..2136ab5f63b 100644 --- a/homeassistant/components/lifx/util.py +++ b/homeassistant/components/lifx/util.py @@ -25,7 +25,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry as dr import homeassistant.util.color as color_util -from .const import _LOGGER, DOMAIN, OVERALL_TIMEOUT +from .const import _LOGGER, DOMAIN, INFRARED_BRIGHTNESS_VALUES_MAP, OVERALL_TIMEOUT FIX_MAC_FW = AwesomeVersion("3.70") @@ -45,6 +45,17 @@ def async_get_legacy_entry(hass: HomeAssistant) -> ConfigEntry | None: return None +def infrared_brightness_value_to_option(value: int) -> str | None: + """Convert infrared brightness from value to option.""" + return INFRARED_BRIGHTNESS_VALUES_MAP.get(value, None) + + +def infrared_brightness_option_to_value(option: str) -> int | None: + """Convert infrared brightness option to value.""" + option_values = {v: k for k, v in INFRARED_BRIGHTNESS_VALUES_MAP.items()} + return option_values.get(option, None) + + def convert_8_to_16(value: int) -> int: """Scale an 8 bit level into 16 bits.""" return (value << 8) | value @@ -70,7 +81,7 @@ def find_hsbk(hass: HomeAssistant, **kwargs: Any) -> list[float | int | None] | """ hue, saturation, brightness, kelvin = [None] * 4 - preprocess_turn_on_alternatives(hass, kwargs) # type: ignore[no-untyped-call] + preprocess_turn_on_alternatives(hass, kwargs) if ATTR_HS_COLOR in kwargs: hue, saturation = kwargs[ATTR_HS_COLOR] diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 33ec3119b95..7d34c607b1f 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -8,7 +8,7 @@ from datetime import timedelta from enum import IntEnum import logging import os -from typing import cast, final +from typing import Any, cast, final import voluptuous as vol @@ -20,7 +20,7 @@ from homeassistant.const import ( SERVICE_TURN_ON, STATE_ON, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.config_validation import ( # noqa: F401 @@ -34,8 +34,6 @@ from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass import homeassistant.util.color as color_util -# mypy: allow-untyped-defs, no-check-untyped-defs - DOMAIN = "light" SCAN_INTERVAL = timedelta(seconds=30) DATA_PROFILES = "light_profiles" @@ -114,6 +112,8 @@ COLOR_MODES_COLOR = { ColorMode.XY, } +# mypy: disallow-any-generics + def filter_supported_color_modes(color_modes: Iterable[ColorMode]) -> set[ColorMode]: """Filter the given color modes.""" @@ -169,7 +169,7 @@ def color_temp_supported(color_modes: Iterable[ColorMode | str] | None) -> bool: return ColorMode.COLOR_TEMP in color_modes -def get_supported_color_modes(hass: HomeAssistant, entity_id: str) -> set | None: +def get_supported_color_modes(hass: HomeAssistant, entity_id: str) -> set[str] | None: """Get supported color modes for a light entity. First try the statemachine, then entity registry. @@ -288,7 +288,9 @@ def is_on(hass: HomeAssistant, entity_id: str) -> bool: return hass.states.is_state(entity_id, STATE_ON) -def preprocess_turn_on_alternatives(hass, params): +def preprocess_turn_on_alternatives( + hass: HomeAssistant, params: dict[str, Any] +) -> None: """Process extra data for turn light on request. Async friendly. @@ -316,7 +318,9 @@ def preprocess_turn_on_alternatives(hass, params): params[ATTR_BRIGHTNESS] = round(255 * brightness_pct / 100) -def filter_turn_off_params(light, params): +def filter_turn_off_params( + light: LightEntity, params: dict[str, Any] +) -> dict[str, Any]: """Filter out params not used in turn off or not supported by the light.""" supported_features = light.supported_features @@ -328,7 +332,7 @@ def filter_turn_off_params(light, params): return {k: v for k, v in params.items() if k in (ATTR_TRANSITION, ATTR_FLASH)} -def filter_turn_on_params(light, params): +def filter_turn_on_params(light: LightEntity, params: dict[str, Any]) -> dict[str, Any]: """Filter out params not supported by the light.""" supported_features = light.supported_features @@ -364,7 +368,7 @@ def filter_turn_on_params(light, params): async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: C901 """Expose light control via state machine and services.""" - component = hass.data[DOMAIN] = EntityComponent( + component = hass.data[DOMAIN] = EntityComponent[LightEntity]( _LOGGER, DOMAIN, hass, SCAN_INTERVAL ) await component.async_setup(config) @@ -372,9 +376,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: profiles = hass.data[DATA_PROFILES] = Profiles(hass) await profiles.async_initialize() - def preprocess_data(data): + def preprocess_data(data: dict[str, Any]) -> dict[str | vol.Optional, Any]: """Preprocess the service data.""" - base = { + base: dict[str | vol.Optional, Any] = { entity_field: data.pop(entity_field) for entity_field in cv.ENTITY_SERVICE_FIELDS if entity_field in data @@ -384,18 +388,20 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: base["params"] = data return base - async def async_handle_light_on_service(light, call): + async def async_handle_light_on_service( + light: LightEntity, call: ServiceCall + ) -> None: """Handle turning a light on. If brightness is set to 0, this service will turn the light off. """ - params = dict(call.data["params"]) + params: dict[str, Any] = dict(call.data["params"]) # Only process params once we processed brightness step if params and ( ATTR_BRIGHTNESS_STEP in params or ATTR_BRIGHTNESS_STEP_PCT in params ): - brightness = light.brightness if light.is_on else 0 + brightness = light.brightness if light.is_on and light.brightness else 0 if ATTR_BRIGHTNESS_STEP in params: brightness += params.pop(ATTR_BRIGHTNESS_STEP) @@ -438,6 +444,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: # If a color is specified, convert to the color space supported by the light # Backwards compatibility: Fall back to hs color if light.supported_color_modes # is not implemented + rgb_color: tuple[int, int, int] | None + rgbww_color: tuple[int, int, int, int, int] | None if not supported_color_modes: if (rgb_color := params.pop(ATTR_RGB_COLOR, None)) is not None: params[ATTR_HS_COLOR] = color_util.color_RGB_to_hs(*rgb_color) @@ -447,7 +455,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: rgb_color = color_util.color_rgbw_to_rgb(*rgbw_color) params[ATTR_HS_COLOR] = color_util.color_RGB_to_hs(*rgb_color) elif (rgbww_color := params.pop(ATTR_RGBWW_COLOR, None)) is not None: - rgb_color = color_util.color_rgbww_to_rgb( + # https://github.com/python/mypy/issues/13673 + rgb_color = color_util.color_rgbww_to_rgb( # type: ignore[call-arg] *rgbww_color, light.min_mireds, light.max_mireds ) params[ATTR_HS_COLOR] = color_util.color_RGB_to_hs(*rgb_color) @@ -466,11 +475,12 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: elif ColorMode.XY in supported_color_modes: params[ATTR_XY_COLOR] = color_util.color_hs_to_xy(*hs_color) elif ATTR_RGB_COLOR in params and ColorMode.RGB not in supported_color_modes: - rgb_color = params.pop(ATTR_RGB_COLOR) + assert (rgb_color := params.pop(ATTR_RGB_COLOR)) is not None if ColorMode.RGBW in supported_color_modes: params[ATTR_RGBW_COLOR] = color_util.color_rgb_to_rgbw(*rgb_color) elif ColorMode.RGBWW in supported_color_modes: - params[ATTR_RGBWW_COLOR] = color_util.color_rgb_to_rgbww( + # 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_mireds, light.max_mireds ) elif ColorMode.HS in supported_color_modes: @@ -507,8 +517,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: elif ( ATTR_RGBWW_COLOR in params and ColorMode.RGBWW not in supported_color_modes ): - rgbww_color = params.pop(ATTR_RGBWW_COLOR) - rgb_color = color_util.color_rgbww_to_rgb( + assert (rgbww_color := params.pop(ATTR_RGBWW_COLOR)) is not None + # https://github.com/python/mypy/issues/13673 + rgb_color = color_util.color_rgbww_to_rgb( # type: ignore[call-arg] *rgbww_color, light.min_mireds, light.max_mireds ) if ColorMode.RGB in supported_color_modes: @@ -534,7 +545,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: else: await light.async_turn_on(**filter_turn_on_params(light, params)) - async def async_handle_light_off_service(light, call): + async def async_handle_light_off_service( + light: LightEntity, call: ServiceCall + ) -> None: """Handle turning off a light.""" params = dict(call.data["params"]) @@ -543,7 +556,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: await light.async_turn_off(**filter_turn_off_params(light, params)) - async def async_handle_toggle_service(light, call): + async def async_handle_toggle_service( + light: LightEntity, call: ServiceCall + ) -> None: """Handle toggling a light.""" if light.is_on: await async_handle_light_off_service(light, call) @@ -575,13 +590,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry.""" - component = cast(EntityComponent, hass.data[DOMAIN]) + component: EntityComponent[LightEntity] = hass.data[DOMAIN] return await component.async_setup_entry(entry) async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - component = cast(EntityComponent, hass.data[DOMAIN]) + component: EntityComponent[LightEntity] = hass.data[DOMAIN] return await component.async_unload_entry(entry) @@ -689,7 +704,9 @@ class Profiles: self.data = await self.hass.async_add_executor_job(self._load_profile_data) @callback - def apply_default(self, entity_id: str, state_on: bool, params: dict) -> None: + def apply_default( + self, entity_id: str, state_on: bool | None, params: dict[str, Any] + ) -> None: """Return the default profile for the given light.""" for _entity_id in (entity_id, "group.all_lights"): name = f"{_entity_id}.default" @@ -700,7 +717,7 @@ class Profiles: params.setdefault(ATTR_TRANSITION, self.data[name].transition) @callback - def apply_profile(self, name: str, params: dict) -> None: + def apply_profile(self, name: str, params: dict[str, Any]) -> None: """Apply a profile.""" if (profile := self.data.get(name)) is None: return @@ -841,9 +858,9 @@ class LightEntity(ToggleEntity): return self._attr_effect @property - def capability_attributes(self): + def capability_attributes(self) -> dict[str, Any]: """Return capability attributes.""" - data = {} + data: dict[str, Any] = {} supported_features = self.supported_features supported_color_modes = self._light_internal_supported_color_modes @@ -858,8 +875,10 @@ class LightEntity(ToggleEntity): return data - def _light_internal_convert_color(self, color_mode: ColorMode | str) -> dict: - data: dict[str, tuple] = {} + def _light_internal_convert_color( + self, color_mode: ColorMode | str + ) -> dict[str, tuple[float, ...]]: + data: dict[str, tuple[float, ...]] = {} if color_mode == ColorMode.HS and self.hs_color: hs_color = self.hs_color data[ATTR_HS_COLOR] = (round(hs_color[0], 3), round(hs_color[1], 3)) @@ -902,12 +921,12 @@ class LightEntity(ToggleEntity): @final @property - def state_attributes(self): + def state_attributes(self) -> dict[str, Any] | None: """Return state attributes.""" if not self.is_on: return None - data = {} + data: dict[str, Any] = {} supported_features = self.supported_features color_mode = self._light_internal_color_mode @@ -977,20 +996,3 @@ class LightEntity(ToggleEntity): def supported_features(self) -> int: """Flag supported features.""" return self._attr_supported_features - - -def legacy_supported_features( - supported_features: int, supported_color_modes: list[str] | None -) -> int: - """Calculate supported features with backwards compatibility.""" - # Backwards compatibility for supported_color_modes added in 2021.4 - if supported_color_modes is None: - return supported_features - if any(mode in supported_color_modes for mode in COLOR_MODES_COLOR): - supported_features |= SUPPORT_COLOR - if any(mode in supported_color_modes for mode in COLOR_MODES_BRIGHTNESS): - supported_features |= SUPPORT_BRIGHTNESS - if ColorMode.COLOR_TEMP in supported_color_modes: - supported_features |= SUPPORT_COLOR_TEMP - - return supported_features diff --git a/homeassistant/components/light/manifest.json b/homeassistant/components/light/manifest.json index c7cf2abc7c8..e49701794d4 100644 --- a/homeassistant/components/light/manifest.json +++ b/homeassistant/components/light/manifest.json @@ -3,5 +3,6 @@ "name": "Light", "documentation": "https://www.home-assistant.io/integrations/light", "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "entity" } diff --git a/homeassistant/components/lightwave/climate.py b/homeassistant/components/lightwave/climate.py index 8076cbc058b..834fc7eaeca 100644 --- a/homeassistant/components/lightwave/climate.py +++ b/homeassistant/components/lightwave/climate.py @@ -1,12 +1,12 @@ """Support for LightwaveRF TRVs.""" from __future__ import annotations +from typing import Any + from homeassistant.components.climate import ( DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP, ClimateEntity, -) -from homeassistant.components.climate.const import ( ClimateEntityFeature, HVACAction, HVACMode, @@ -61,7 +61,7 @@ class LightwaveTrv(ClimateEntity): # inhibit is used to prevent race condition on update. If non zero, skip next update cycle. self._inhibit = 0 - def update(self): + def update(self) -> None: """Communicate with a Lightwave RTF Proxy to get state.""" (temp, targ, _, trv_output) = self._lwlink.read_trv_status(self._serial) if temp is not None: @@ -95,7 +95,7 @@ class LightwaveTrv(ClimateEntity): self._attr_target_temperature = self._inhibit return self._attr_target_temperature - def set_temperature(self, **kwargs): + def set_temperature(self, **kwargs: Any) -> None: """Set TRV target temperature.""" if ATTR_TEMPERATURE in kwargs: self._attr_target_temperature = kwargs[ATTR_TEMPERATURE] diff --git a/homeassistant/components/lightwave/sensor.py b/homeassistant/components/lightwave/sensor.py index 14ea3bb85a8..dac591aea34 100644 --- a/homeassistant/components/lightwave/sensor.py +++ b/homeassistant/components/lightwave/sensor.py @@ -50,7 +50,7 @@ class LightwaveBattery(SensorEntity): self._serial = serial self._attr_unique_id = f"{serial}-trv-battery" - def update(self): + def update(self) -> None: """Communicate with a Lightwave RTF Proxy to get state.""" (dummy_temp, dummy_targ, battery, dummy_output) = self._lwlink.read_trv_status( self._serial diff --git a/homeassistant/components/lightwave/switch.py b/homeassistant/components/lightwave/switch.py index 80cc80510a4..67b69d0e5c4 100644 --- a/homeassistant/components/lightwave/switch.py +++ b/homeassistant/components/lightwave/switch.py @@ -1,6 +1,8 @@ """Support for LightwaveRF switches.""" from __future__ import annotations +from typing import Any + from homeassistant.components.switch import SwitchEntity from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant @@ -41,13 +43,13 @@ class LWRFSwitch(SwitchEntity): self._device_id = device_id self._lwlink = lwlink - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the LightWave switch on.""" self._attr_is_on = True self._lwlink.turn_on_switch(self._device_id, self._attr_name) self.async_write_ha_state() - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the LightWave switch off.""" self._attr_is_on = False self._lwlink.turn_off(self._device_id, self._attr_name) diff --git a/homeassistant/components/linksys_smart/device_tracker.py b/homeassistant/components/linksys_smart/device_tracker.py index 8b296404532..3b0aeffaa6d 100644 --- a/homeassistant/components/linksys_smart/device_tracker.py +++ b/homeassistant/components/linksys_smart/device_tracker.py @@ -24,7 +24,9 @@ _LOGGER = logging.getLogger(__name__) PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend({vol.Required(CONF_HOST): cv.string}) -def get_scanner(hass: HomeAssistant, config: ConfigType) -> DeviceScanner | None: +def get_scanner( + hass: HomeAssistant, config: ConfigType +) -> LinksysSmartWifiDeviceScanner | None: """Validate the configuration and return a Linksys AP scanner.""" try: return LinksysSmartWifiDeviceScanner(config[DOMAIN]) diff --git a/homeassistant/components/linode/binary_sensor.py b/homeassistant/components/linode/binary_sensor.py index 67ccde764e1..2c63bbc0bc8 100644 --- a/homeassistant/components/linode/binary_sensor.py +++ b/homeassistant/components/linode/binary_sensor.py @@ -68,7 +68,7 @@ class LinodeBinarySensor(BinarySensorEntity): self._attr_extra_state_attributes = {} self._attr_name = None - def update(self): + def update(self) -> None: """Update state of sensor.""" data = None self._linode.update() diff --git a/homeassistant/components/linode/switch.py b/homeassistant/components/linode/switch.py index 76cd95e5bca..183abbc068c 100644 --- a/homeassistant/components/linode/switch.py +++ b/homeassistant/components/linode/switch.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any import voluptuous as vol @@ -63,17 +64,17 @@ class LinodeSwitch(SwitchEntity): self.data = None self._attr_extra_state_attributes = {} - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Boot-up the Node.""" if self.data.status != "running": self.data.boot() - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Shutdown the nodes.""" if self.data.status == "running": self.data.shutdown() - def update(self): + def update(self) -> None: """Get the latest data from the device and update the data.""" self._linode.update() if self._linode.data is not None: diff --git a/homeassistant/components/linux_battery/sensor.py b/homeassistant/components/linux_battery/sensor.py index ec747778b7b..765e0d79537 100644 --- a/homeassistant/components/linux_battery/sensor.py +++ b/homeassistant/components/linux_battery/sensor.py @@ -124,7 +124,7 @@ class LinuxBatterySensor(SensorEntity): ATTR_VOLTAGE_NOW: self._battery_stat.voltage_now, } - def update(self): + def update(self) -> None: """Get the latest data and updates the states.""" self._battery.update() self._battery_stat = self._battery.stat[self._battery_id] diff --git a/homeassistant/components/litejet/switch.py b/homeassistant/components/litejet/switch.py index 66b68a345f2..375e3dd9f46 100644 --- a/homeassistant/components/litejet/switch.py +++ b/homeassistant/components/litejet/switch.py @@ -1,5 +1,6 @@ """Support for LiteJet switch.""" import logging +from typing import Any from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry @@ -45,12 +46,12 @@ class LiteJetSwitch(SwitchEntity): self._state = False self._name = name - async def async_added_to_hass(self): + 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) - async def async_will_remove_from_hass(self): + 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) @@ -85,11 +86,11 @@ class LiteJetSwitch(SwitchEntity): """Return the device-specific state attributes.""" return {ATTR_NUMBER: self._index} - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Press the switch.""" self._lj.press_switch(self._index) - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Release the switch.""" self._lj.release_switch(self._index) diff --git a/homeassistant/components/litterrobot/__init__.py b/homeassistant/components/litterrobot/__init__.py index 742e9dcb9c7..3d8f8487b33 100644 --- a/homeassistant/components/litterrobot/__init__.py +++ b/homeassistant/components/litterrobot/__init__.py @@ -12,6 +12,7 @@ from .hub import LitterRobotHub PLATFORMS_BY_TYPE = { Robot: ( + Platform.BINARY_SENSOR, Platform.SELECT, Platform.SENSOR, Platform.SWITCH, @@ -37,7 +38,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Litter-Robot from a config entry.""" hass.data.setdefault(DOMAIN, {}) hub = hass.data[DOMAIN][entry.entry_id] = LitterRobotHub(hass, entry.data) - await hub.login(load_robots=True) + await hub.login(load_robots=True, subscribe_for_updates=True) if platforms := get_platforms_for_robots(hub.account.robots): await hass.config_entries.async_forward_entry_setups(entry, platforms) diff --git a/homeassistant/components/litterrobot/binary_sensor.py b/homeassistant/components/litterrobot/binary_sensor.py new file mode 100644 index 00000000000..781cfb73b7f --- /dev/null +++ b/homeassistant/components/litterrobot/binary_sensor.py @@ -0,0 +1,95 @@ +"""Support for Litter-Robot binary sensors.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +from typing import Generic + +from pylitterbot import LitterRobot, Robot + +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 .entity import LitterRobotEntity, _RobotT +from .hub import LitterRobotHub + + +@dataclass +class RequiredKeysMixin(Generic[_RobotT]): + """A class that describes robot binary sensor entity required keys.""" + + is_on_fn: Callable[[_RobotT], bool] + + +@dataclass +class RobotBinarySensorEntityDescription( + BinarySensorEntityDescription, RequiredKeysMixin[_RobotT] +): + """A class that describes robot binary sensor entities.""" + + +class LitterRobotBinarySensorEntity(LitterRobotEntity[_RobotT], BinarySensorEntity): + """Litter-Robot binary sensor entity.""" + + entity_description: RobotBinarySensorEntityDescription[_RobotT] + + @property + def is_on(self) -> bool: + """Return the state.""" + return self.entity_description.is_on_fn(self.robot) + + +BINARY_SENSOR_MAP: dict[type[Robot], tuple[RobotBinarySensorEntityDescription, ...]] = { + LitterRobot: ( + RobotBinarySensorEntityDescription[LitterRobot]( + key="sleeping", + name="Sleeping", + icon="mdi:sleep", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + is_on_fn=lambda robot: robot.is_sleeping, + ), + RobotBinarySensorEntityDescription[LitterRobot]( + key="sleep_mode", + name="Sleep mode", + icon="mdi:sleep", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + is_on_fn=lambda robot: robot.sleep_mode_enabled, + ), + ), + Robot: ( + RobotBinarySensorEntityDescription[Robot]( + key="power_status", + name="Power status", + device_class=BinarySensorDeviceClass.PLUG, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + is_on_fn=lambda robot: robot.power_status == "AC", + ), + ), +} + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Litter-Robot binary sensors using config entry.""" + hub: LitterRobotHub = hass.data[DOMAIN][entry.entry_id] + async_add_entities( + LitterRobotBinarySensorEntity(robot=robot, hub=hub, description=description) + for robot in hub.account.robots + for robot_type, entity_descriptions in BINARY_SENSOR_MAP.items() + if isinstance(robot, robot_type) + for description in entity_descriptions + ) diff --git a/homeassistant/components/litterrobot/entity.py b/homeassistant/components/litterrobot/entity.py index 9716793f70e..3ad21b1aeb7 100644 --- a/homeassistant/components/litterrobot/entity.py +++ b/homeassistant/components/litterrobot/entity.py @@ -1,33 +1,24 @@ """Litter-Robot entities for common data and methods.""" from __future__ import annotations -from collections.abc import Callable, Coroutine, Iterable -from datetime import time -import logging -from typing import Any, Generic, TypeVar +from collections.abc import Iterable +from typing import Generic, TypeVar from pylitterbot import Robot -from pylitterbot.exceptions import InvalidCommandException -from typing_extensions import ParamSpec +from pylitterbot.robot import EVENT_UPDATE -from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback -from homeassistant.helpers.entity import DeviceInfo, EntityCategory, EntityDescription +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo, EntityDescription import homeassistant.helpers.entity_registry as er -from homeassistant.helpers.event import async_call_later from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, ) -import homeassistant.util.dt as dt_util from .const import DOMAIN from .hub import LitterRobotHub -_P = ParamSpec("_P") _RobotT = TypeVar("_RobotT", bound=Robot) -_LOGGER = logging.getLogger(__name__) - -REFRESH_WAIT_TIME_SECONDS = 8 class LitterRobotEntity( @@ -62,95 +53,10 @@ class LitterRobotEntity( sw_version=getattr(self.robot, "firmware", None), ) - -class LitterRobotControlEntity(LitterRobotEntity[_RobotT]): - """A Litter-Robot entity that can control the unit.""" - - def __init__( - self, robot: _RobotT, hub: LitterRobotHub, description: EntityDescription - ) -> None: - """Init a Litter-Robot control entity.""" - super().__init__(robot=robot, hub=hub, description=description) - self._refresh_callback: CALLBACK_TYPE | None = None - - async def perform_action_and_refresh( - self, - action: Callable[_P, Coroutine[Any, Any, bool]], - *args: _P.args, - **kwargs: _P.kwargs, - ) -> bool: - """Perform an action and initiates a refresh of the robot data after a few seconds.""" - success = False - - try: - success = await action(*args, **kwargs) - except InvalidCommandException as ex: # pragma: no cover - # this exception should only occur if the underlying API for commands changes - _LOGGER.error(ex) - success = False - - if success: - self.async_cancel_refresh_callback() - self._refresh_callback = async_call_later( - self.hass, REFRESH_WAIT_TIME_SECONDS, self.async_call_later_callback - ) - return success - - async def async_call_later_callback(self, *_: Any) -> None: - """Perform refresh request on callback.""" - self._refresh_callback = None - await self.coordinator.async_request_refresh() - - async def async_will_remove_from_hass(self) -> None: - """Cancel refresh callback when entity is being removed from hass.""" - self.async_cancel_refresh_callback() - - @callback - def async_cancel_refresh_callback(self) -> None: - """Clear the refresh callback if it has not already fired.""" - if self._refresh_callback is not None: - self._refresh_callback() - self._refresh_callback = None - - @staticmethod - def parse_time_at_default_timezone(time_str: str | None) -> time | None: - """Parse a time string and add default timezone.""" - if time_str is None: - return None - - if (parsed_time := dt_util.parse_time(time_str)) is None: # pragma: no cover - return None - - return ( - dt_util.start_of_local_day() - .replace( - hour=parsed_time.hour, - minute=parsed_time.minute, - second=parsed_time.second, - ) - .timetz() - ) - - -class LitterRobotConfigEntity(LitterRobotControlEntity[_RobotT]): - """A Litter-Robot entity that can control configuration of the unit.""" - - _attr_entity_category = EntityCategory.CONFIG - - def __init__( - self, robot: _RobotT, hub: LitterRobotHub, description: EntityDescription - ) -> None: - """Init a Litter-Robot control entity.""" - super().__init__(robot=robot, hub=hub, description=description) - self._assumed_state: bool | None = None - - async def perform_action_and_assume_state( - self, action: Callable[[bool], Coroutine[Any, Any, bool]], assumed_state: bool - ) -> None: - """Perform an action and assume the state passed in if call is successful.""" - if await self.perform_action_and_refresh(action, assumed_state): - self._assumed_state = assumed_state - self.async_write_ha_state() + async def async_added_to_hass(self) -> None: + """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( diff --git a/homeassistant/components/litterrobot/hub.py b/homeassistant/components/litterrobot/hub.py index 8fab3346cec..5dc8098a8df 100644 --- a/homeassistant/components/litterrobot/hub.py +++ b/homeassistant/components/litterrobot/hub.py @@ -19,7 +19,7 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -UPDATE_INTERVAL_SECONDS = 20 +UPDATE_INTERVAL_SECONDS = 60 * 5 class LitterRobotHub: @@ -43,13 +43,16 @@ class LitterRobotHub: update_interval=timedelta(seconds=UPDATE_INTERVAL_SECONDS), ) - async def login(self, load_robots: bool = False) -> None: + async def login( + self, load_robots: bool = False, subscribe_for_updates: bool = False + ) -> None: """Login to Litter-Robot.""" try: await self.account.connect( username=self._data[CONF_USERNAME], password=self._data[CONF_PASSWORD], load_robots=load_robots, + subscribe_for_updates=subscribe_for_updates, ) return except LitterRobotLoginException as ex: diff --git a/homeassistant/components/litterrobot/manifest.json b/homeassistant/components/litterrobot/manifest.json index a4c9f3cd54e..ed813983674 100644 --- a/homeassistant/components/litterrobot/manifest.json +++ b/homeassistant/components/litterrobot/manifest.json @@ -3,9 +3,9 @@ "name": "Litter-Robot", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/litterrobot", - "requirements": ["pylitterbot==2022.9.1"], + "requirements": ["pylitterbot==2022.9.6"], "codeowners": ["@natekspencer", "@tkdrob"], "dhcp": [{ "hostname": "litter-robot4" }], - "iot_class": "cloud_polling", + "iot_class": "cloud_push", "loggers": ["pylitterbot"] } diff --git a/homeassistant/components/litterrobot/select.py b/homeassistant/components/litterrobot/select.py index 9ec784db8f2..d384e94a092 100644 --- a/homeassistant/components/litterrobot/select.py +++ b/homeassistant/components/litterrobot/select.py @@ -16,10 +16,11 @@ from homeassistant.components.select import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import TIME_MINUTES from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN -from .entity import LitterRobotConfigEntity, _RobotT, async_update_unique_id +from .entity import LitterRobotEntity, _RobotT, async_update_unique_id from .hub import LitterRobotHub _CastTypeT = TypeVar("_CastTypeT", int, float) @@ -31,10 +32,7 @@ class RequiredKeysMixin(Generic[_RobotT, _CastTypeT]): current_fn: Callable[[_RobotT], _CastTypeT] options_fn: Callable[[_RobotT], list[_CastTypeT]] - select_fn: Callable[ - [_RobotT, str], - tuple[Callable[[_CastTypeT], Coroutine[Any, Any, bool]], _CastTypeT], - ] + select_fn: Callable[[_RobotT, str], Coroutine[Any, Any, bool]] @dataclass @@ -43,6 +41,8 @@ class RobotSelectEntityDescription( ): """A class that describes robot select entities.""" + entity_category: EntityCategory = EntityCategory.CONFIG + LITTER_ROBOT_SELECT = RobotSelectEntityDescription[LitterRobot, int]( key="cycle_delay", @@ -51,7 +51,7 @@ LITTER_ROBOT_SELECT = RobotSelectEntityDescription[LitterRobot, int]( unit_of_measurement=TIME_MINUTES, current_fn=lambda robot: robot.clean_cycle_wait_time_minutes, options_fn=lambda robot: robot.VALID_WAIT_TIMES, - select_fn=lambda robot, option: (robot.set_wait_time, int(option)), + select_fn=lambda robot, option: robot.set_wait_time(int(option)), ) FEEDER_ROBOT_SELECT = RobotSelectEntityDescription[FeederRobot, float]( key="meal_insert_size", @@ -60,7 +60,7 @@ FEEDER_ROBOT_SELECT = RobotSelectEntityDescription[FeederRobot, float]( unit_of_measurement="cups", current_fn=lambda robot: robot.meal_insert_size, options_fn=lambda robot: robot.VALID_MEAL_INSERT_SIZES, - select_fn=lambda robot, option: (robot.set_meal_insert_size, float(option)), + select_fn=lambda robot, option: robot.set_meal_insert_size(float(option)), ) @@ -88,7 +88,7 @@ async def async_setup_entry( class LitterRobotSelect( - LitterRobotConfigEntity[_RobotT], SelectEntity, Generic[_RobotT, _CastTypeT] + LitterRobotEntity[_RobotT], SelectEntity, Generic[_RobotT, _CastTypeT] ): """Litter-Robot Select.""" @@ -112,5 +112,4 @@ class LitterRobotSelect( async def async_select_option(self, option: str) -> None: """Change the selected option.""" - action, adjusted_option = self.entity_description.select_fn(self.robot, option) - await self.perform_action_and_refresh(action, adjusted_option) + await self.entity_description.select_fn(self.robot, option) diff --git a/homeassistant/components/litterrobot/sensor.py b/homeassistant/components/litterrobot/sensor.py index c904335d23f..1857931143d 100644 --- a/homeassistant/components/litterrobot/sensor.py +++ b/homeassistant/components/litterrobot/sensor.py @@ -100,12 +100,18 @@ ROBOT_SENSOR_MAP: dict[type[Robot], list[RobotSensorEntityDescription]] = { ), ], LitterRobot4: [ + RobotSensorEntityDescription[LitterRobot4]( + key="litter_level", + name="Litter level", + native_unit_of_measurement=PERCENTAGE, + icon_fn=lambda state: icon_for_gauge_level(state, 10), + ), RobotSensorEntityDescription[LitterRobot4]( key="pet_weight", name="Pet weight", - icon="mdi:scale", native_unit_of_measurement=MASS_POUNDS, - ) + device_class=SensorDeviceClass.WEIGHT, + ), ], FeederRobot: [ RobotSensorEntityDescription[FeederRobot]( diff --git a/homeassistant/components/litterrobot/strings.json b/homeassistant/components/litterrobot/strings.json index 140a0308188..f2256249b8e 100644 --- a/homeassistant/components/litterrobot/strings.json +++ b/homeassistant/components/litterrobot/strings.json @@ -24,5 +24,11 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } + }, + "issues": { + "migrated_attributes": { + "title": "Litter-Robot attributes are now their own sensors", + "description": "The vacuum entity attributes are now available as diagnostic sensors.\n\nPlease adjust any automations or scripts you may have that use these attributes." + } } } diff --git a/homeassistant/components/litterrobot/strings.sensor.json b/homeassistant/components/litterrobot/strings.sensor.json index d9ad141cf21..0c901704b02 100644 --- a/homeassistant/components/litterrobot/strings.sensor.json +++ b/homeassistant/components/litterrobot/strings.sensor.json @@ -4,6 +4,7 @@ "br": "Bonnet Removed", "ccc": "Clean Cycle Complete", "ccp": "Clean Cycle In Progress", + "cd": "Cat Detected", "csf": "Cat Sensor Fault", "csi": "Cat Sensor Interrupted", "cst": "Cat Sensor Timing", @@ -19,6 +20,8 @@ "otf": "Over Torque Fault", "p": "[%key:common::state::paused%]", "pd": "Pinch Detect", + "pwrd": "Powering Down", + "pwru": "Powering Up", "rdy": "Ready", "scf": "Cat Sensor Fault At Startup", "sdf": "Drawer Full At Startup", diff --git a/homeassistant/components/litterrobot/switch.py b/homeassistant/components/litterrobot/switch.py index 779ee699b41..af690f30501 100644 --- a/homeassistant/components/litterrobot/switch.py +++ b/homeassistant/components/litterrobot/switch.py @@ -14,10 +14,11 @@ from homeassistant.components.switch 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 .const import DOMAIN -from .entity import LitterRobotConfigEntity, _RobotT, async_update_unique_id +from .entity import LitterRobotEntity, _RobotT, async_update_unique_id from .hub import LitterRobotHub @@ -26,31 +27,33 @@ class RequiredKeysMixin(Generic[_RobotT]): """A class that describes robot switch entity required keys.""" icons: tuple[str, str] - set_fn: Callable[[_RobotT], Callable[[bool], Coroutine[Any, Any, bool]]] + set_fn: Callable[[_RobotT, bool], Coroutine[Any, Any, bool]] @dataclass class RobotSwitchEntityDescription(SwitchEntityDescription, RequiredKeysMixin[_RobotT]): """A class that describes robot switch entities.""" + entity_category: EntityCategory = EntityCategory.CONFIG + ROBOT_SWITCHES = [ RobotSwitchEntityDescription[Union[LitterRobot, FeederRobot]]( key="night_light_mode_enabled", name="Night Light Mode", icons=("mdi:lightbulb-on", "mdi:lightbulb-off"), - set_fn=lambda robot: robot.set_night_light, + set_fn=lambda robot, value: robot.set_night_light(value), ), RobotSwitchEntityDescription[Union[LitterRobot, FeederRobot]]( key="panel_lock_enabled", name="Panel Lockout", icons=("mdi:lock", "mdi:lock-open"), - set_fn=lambda robot: robot.set_panel_lockout, + set_fn=lambda robot, value: robot.set_panel_lockout(value), ), ] -class RobotSwitchEntity(LitterRobotConfigEntity[_RobotT], SwitchEntity): +class RobotSwitchEntity(LitterRobotEntity[_RobotT], SwitchEntity): """Litter-Robot switch entity.""" entity_description: RobotSwitchEntityDescription[_RobotT] @@ -58,8 +61,6 @@ class RobotSwitchEntity(LitterRobotConfigEntity[_RobotT], SwitchEntity): @property def is_on(self) -> bool | None: """Return true if switch is on.""" - if self._refresh_callback is not None: - return self._assumed_state return bool(getattr(self.robot, self.entity_description.key)) @property @@ -70,13 +71,11 @@ class RobotSwitchEntity(LitterRobotConfigEntity[_RobotT], SwitchEntity): async def async_turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" - set_fn = self.entity_description.set_fn - await self.perform_action_and_assume_state(set_fn(self.robot), True) + await self.entity_description.set_fn(self.robot, True) async def async_turn_off(self, **kwargs: Any) -> None: """Turn the switch off.""" - set_fn = self.entity_description.set_fn - await self.perform_action_and_assume_state(set_fn(self.robot), False) + await self.entity_description.set_fn(self.robot, False) async def async_setup_entry( diff --git a/homeassistant/components/litterrobot/translations/bg.json b/homeassistant/components/litterrobot/translations/bg.json index bad1fba5a87..7664989fae5 100644 --- a/homeassistant/components/litterrobot/translations/bg.json +++ b/homeassistant/components/litterrobot/translations/bg.json @@ -1,9 +1,19 @@ { "config": { + "abort": { + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" + }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" }, "step": { + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430" + }, + "description": "\u041c\u043e\u043b\u044f, \u0430\u043a\u0442\u0443\u0430\u043b\u0438\u0437\u0438\u0440\u0430\u0439\u0442\u0435 \u043f\u0430\u0440\u043e\u043b\u0430\u0442\u0430 \u0441\u0438 \u0437\u0430 {username}", + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u043d\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430" + }, "user": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u0430", diff --git a/homeassistant/components/litterrobot/translations/cs.json b/homeassistant/components/litterrobot/translations/cs.json index b6c00c05389..e5d6edc65ea 100644 --- a/homeassistant/components/litterrobot/translations/cs.json +++ b/homeassistant/components/litterrobot/translations/cs.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u00da\u010det je ji\u017e nastaven" + "already_configured": "\u00da\u010det je ji\u017e nastaven", + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" }, "error": { "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", @@ -9,6 +10,12 @@ "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, "step": { + "reauth_confirm": { + "data": { + "password": "Heslo" + }, + "title": "Znovu ov\u011b\u0159it integraci" + }, "user": { "data": { "password": "Heslo", diff --git a/homeassistant/components/litterrobot/translations/de.json b/homeassistant/components/litterrobot/translations/de.json index 18bb6458cf3..8259aa1d16c 100644 --- a/homeassistant/components/litterrobot/translations/de.json +++ b/homeassistant/components/litterrobot/translations/de.json @@ -24,5 +24,11 @@ } } } + }, + "issues": { + "migrated_attributes": { + "description": "Die Vakuumentit\u00e4tsattribute sind jetzt als Diagnosesensoren verf\u00fcgbar. \n\nBitte passe eventuell vorhandene Automatisierungen oder Skripte an, die diese Attribute verwenden.", + "title": "Litter-Robot-Attribute sind jetzt ihre eigenen Sensoren" + } } } \ No newline at end of file diff --git a/homeassistant/components/litterrobot/translations/en.json b/homeassistant/components/litterrobot/translations/en.json index 3d6ab4dfaaa..76c0dcb79c9 100644 --- a/homeassistant/components/litterrobot/translations/en.json +++ b/homeassistant/components/litterrobot/translations/en.json @@ -24,5 +24,11 @@ } } } + }, + "issues": { + "migrated_attributes": { + "description": "The vacuum entity attributes are now available as diagnostic sensors.\n\nPlease adjust any automations or scripts you may have that use these attributes.", + "title": "Litter-Robot attributes are now their own sensors" + } } } \ No newline at end of file diff --git a/homeassistant/components/litterrobot/translations/es.json b/homeassistant/components/litterrobot/translations/es.json index 003a715f8a7..6ece2f2c221 100644 --- a/homeassistant/components/litterrobot/translations/es.json +++ b/homeassistant/components/litterrobot/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "error": { "cannot_connect": "No se pudo conectar", @@ -24,5 +24,11 @@ } } } + }, + "issues": { + "migrated_attributes": { + "description": "Los atributos de la entidad aspiradora ahora est\u00e1n disponibles como sensores de diagn\u00f3stico. \n\nPor favor, ajusta cualquier automatizaci\u00f3n o script que puedas tener que use estos atributos.", + "title": "Los atributos de Litter-Robot ahora son sus propios sensores" + } } } \ No newline at end of file diff --git a/homeassistant/components/litterrobot/translations/hu.json b/homeassistant/components/litterrobot/translations/hu.json index cc0c820facf..c35fc251f8a 100644 --- a/homeassistant/components/litterrobot/translations/hu.json +++ b/homeassistant/components/litterrobot/translations/hu.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van" + "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt." }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", @@ -9,6 +10,13 @@ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "step": { + "reauth_confirm": { + "data": { + "password": "Jelsz\u00f3" + }, + "description": "K\u00e9rem, friss\u00edtse {username} jelszav\u00e1t", + "title": "Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se" + }, "user": { "data": { "password": "Jelsz\u00f3", @@ -16,5 +24,11 @@ } } } + }, + "issues": { + "migrated_attributes": { + "description": "A az entit\u00e1s attrib\u00fatumok mostant\u00f3l diagnosztikai \u00e9rz\u00e9kel\u0151k\u00e9nt \u00e1llnak rendelkez\u00e9sre.\n\nK\u00e9rrem, korrig\u00e1lja azokat az automatizmusokat vagy szkripteket, amelyek ezeket az attrib\u00fatumokat haszn\u00e1lj\u00e1k.", + "title": "A Litter-Robot attrib\u00fatumok most m\u00e1r \u00f6n\u00e1ll\u00f3 \u00e9rz\u00e9kel\u0151k lettek" + } } } \ No newline at end of file diff --git a/homeassistant/components/litterrobot/translations/id.json b/homeassistant/components/litterrobot/translations/id.json index 4a84db42a14..73e19f1d439 100644 --- a/homeassistant/components/litterrobot/translations/id.json +++ b/homeassistant/components/litterrobot/translations/id.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Perangkat sudah dikonfigurasi" + "already_configured": "Perangkat sudah dikonfigurasi", + "reauth_successful": "Autentikasi ulang berhasil" }, "error": { "cannot_connect": "Gagal terhubung", @@ -9,6 +10,13 @@ "unknown": "Kesalahan yang tidak diharapkan" }, "step": { + "reauth_confirm": { + "data": { + "password": "Kata Sandi" + }, + "description": "Perbarui kata sandi Anda untuk {username}", + "title": "Autentikasi Ulang Integrasi" + }, "user": { "data": { "password": "Kata Sandi", diff --git a/homeassistant/components/litterrobot/translations/ja.json b/homeassistant/components/litterrobot/translations/ja.json index 6972cf2318a..0106d4b63c6 100644 --- a/homeassistant/components/litterrobot/translations/ja.json +++ b/homeassistant/components/litterrobot/translations/ja.json @@ -14,6 +14,7 @@ "data": { "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" }, + "description": "{username} \u306e\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u66f4\u65b0\u3057\u3066\u304f\u3060\u3055\u3044", "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" }, "user": { diff --git a/homeassistant/components/litterrobot/translations/nl.json b/homeassistant/components/litterrobot/translations/nl.json index 50b4c3f2fe6..09d9c66090d 100644 --- a/homeassistant/components/litterrobot/translations/nl.json +++ b/homeassistant/components/litterrobot/translations/nl.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Apparaat is al geconfigureerd" + "already_configured": "Apparaat is al geconfigureerd", + "reauth_successful": "Herauthenticatie geslaagd" }, "error": { "cannot_connect": "Kan geen verbinding maken", @@ -9,6 +10,12 @@ "unknown": "Onverwachte fout" }, "step": { + "reauth_confirm": { + "data": { + "password": "Wachtwoord" + }, + "title": "Integratie herauthenticeren" + }, "user": { "data": { "password": "Wachtwoord", diff --git a/homeassistant/components/litterrobot/translations/no.json b/homeassistant/components/litterrobot/translations/no.json index 40e3013cf45..6268bdf0ff7 100644 --- a/homeassistant/components/litterrobot/translations/no.json +++ b/homeassistant/components/litterrobot/translations/no.json @@ -24,5 +24,11 @@ } } } + }, + "issues": { + "migrated_attributes": { + "description": "Vakuumenhetsattributtene er n\u00e5 tilgjengelige som diagnostiske sensorer. \n\n Juster eventuelle automatiseringer eller skript du m\u00e5tte ha som bruker disse attributtene.", + "title": "Litter-Robot-attributter er n\u00e5 deres egne sensorer" + } } } \ No newline at end of file diff --git a/homeassistant/components/litterrobot/translations/pl.json b/homeassistant/components/litterrobot/translations/pl.json index c0a83866936..306acce8743 100644 --- a/homeassistant/components/litterrobot/translations/pl.json +++ b/homeassistant/components/litterrobot/translations/pl.json @@ -14,6 +14,7 @@ "data": { "password": "Has\u0142o" }, + "description": "Zaktualizuj has\u0142o dla u\u017cytkownika {username}", "title": "Ponownie uwierzytelnij integracj\u0119" }, "user": { diff --git a/homeassistant/components/litterrobot/translations/ru.json b/homeassistant/components/litterrobot/translations/ru.json index a336adcc787..7df59645cb8 100644 --- a/homeassistant/components/litterrobot/translations/ru.json +++ b/homeassistant/components/litterrobot/translations/ru.json @@ -24,5 +24,11 @@ } } } + }, + "issues": { + "migrated_attributes": { + "description": "\u0410\u0442\u0440\u0438\u0431\u0443\u0442\u044b \u043e\u0431\u044a\u0435\u043a\u0442\u0430 \u043f\u044b\u043b\u0435\u0441\u043e\u0441\u0430 \u0442\u0435\u043f\u0435\u0440\u044c \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b \u043a\u0430\u043a \u0441\u0435\u043d\u0441\u043e\u0440\u044b \u0434\u0438\u0430\u0433\u043d\u043e\u0441\u0442\u0438\u043a\u0438.\n\u041e\u0442\u0440\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u0443\u0439\u0442\u0435 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0438\u0438 \u0438 \u0441\u043a\u0440\u0438\u043f\u0442\u044b, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044e\u0449\u0438\u0435 \u044d\u0442\u0438 \u0430\u0442\u0440\u0438\u0431\u0443\u0442\u044b.", + "title": "\u0410\u0442\u0440\u0438\u0431\u0443\u0442\u044b Litter-Robot \u0442\u0435\u043f\u0435\u0440\u044c \u043f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u044b \u043a\u0430\u043a \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u044b\u0435 \u0441\u0435\u043d\u0441\u043e\u0440\u044b." + } } } \ No newline at end of file diff --git a/homeassistant/components/litterrobot/translations/sensor.ca.json b/homeassistant/components/litterrobot/translations/sensor.ca.json index dd8731b78c5..6749f6af0a0 100644 --- a/homeassistant/components/litterrobot/translations/sensor.ca.json +++ b/homeassistant/components/litterrobot/translations/sensor.ca.json @@ -4,6 +4,7 @@ "br": "Bossa extreta", "ccc": "Cicle de neteja completat", "ccp": "Cicle de neteja en curs", + "cd": "Gat detectat", "csf": "Error del sensor de gat", "csi": "Sensor de gat interromput", "cst": "Temps del sensor de gats", @@ -19,6 +20,8 @@ "otf": "Error per sobre-parell", "p": "Pausat/ada", "pd": "Detecci\u00f3 de pessigada", + "pwrd": "Apagant", + "pwru": "Engegant", "rdy": "A punt", "scf": "Error del sensor de gat a l'arrencada", "sdf": "Dip\u00f2sit ple a l'arrencada", diff --git a/homeassistant/components/litterrobot/translations/sensor.de.json b/homeassistant/components/litterrobot/translations/sensor.de.json index 2901b0e5c55..078faa79d6a 100644 --- a/homeassistant/components/litterrobot/translations/sensor.de.json +++ b/homeassistant/components/litterrobot/translations/sensor.de.json @@ -4,6 +4,7 @@ "br": "Haube entfernt", "ccc": "Reinigungszyklus abgeschlossen", "ccp": "Reinigungszyklus l\u00e4uft", + "cd": "Katze erkannt", "csf": "Katzensensor Fehler", "csi": "Katzensensor unterbrochen", "cst": "Katzensensor Timing", @@ -19,6 +20,8 @@ "otf": "\u00dcberdrehungsfehler", "p": "Pausiert", "pd": "Einklemmen erkennen", + "pwrd": "F\u00e4hrt herunter", + "pwru": "F\u00e4hrt hoch", "rdy": "Bereit", "scf": "Katzen-Sensorfehler beim Start", "sdf": "Schublade voll beim Start", diff --git a/homeassistant/components/litterrobot/translations/sensor.el.json b/homeassistant/components/litterrobot/translations/sensor.el.json index f34d6078495..d140630ea24 100644 --- a/homeassistant/components/litterrobot/translations/sensor.el.json +++ b/homeassistant/components/litterrobot/translations/sensor.el.json @@ -4,6 +4,7 @@ "br": "\u03a4\u03bf \u03ba\u03b1\u03c0\u03cc \u03b1\u03c6\u03b1\u03b9\u03c1\u03ad\u03b8\u03b7\u03ba\u03b5", "ccc": "\u039f\u03bb\u03bf\u03ba\u03bb\u03b7\u03c1\u03ce\u03b8\u03b7\u03ba\u03b5 \u03bf \u03ba\u03cd\u03ba\u03bb\u03bf\u03c2 \u03ba\u03b1\u03b8\u03b1\u03c1\u03b9\u03c3\u03bc\u03bf\u03cd", "ccp": "\u039a\u03cd\u03ba\u03bb\u03bf\u03c2 \u03ba\u03b1\u03b8\u03b1\u03c1\u03b9\u03c3\u03bc\u03bf\u03cd \u03c3\u03b5 \u03b5\u03be\u03ad\u03bb\u03b9\u03be\u03b7", + "cd": "\u0391\u03bd\u03b9\u03c7\u03bd\u03b5\u03cd\u03c4\u03b7\u03ba\u03b5 \u03b3\u03ac\u03c4\u03b1", "csf": "\u03a3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03b3\u03ac\u03c4\u03b1\u03c2", "csi": "\u0394\u03b9\u03b1\u03ba\u03bf\u03c0\u03ae \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03b3\u03ac\u03c4\u03b1\u03c2", "cst": "\u03a7\u03c1\u03bf\u03bd\u03b9\u03c3\u03bc\u03cc\u03c2 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03b3\u03ac\u03c4\u03b1\u03c2", @@ -19,6 +20,8 @@ "otf": "\u0392\u03bb\u03ac\u03b2\u03b7 \u03c5\u03c0\u03b5\u03c1\u03b2\u03bf\u03bb\u03b9\u03ba\u03ae\u03c2 \u03c1\u03bf\u03c0\u03ae\u03c2", "p": "\u03a3\u03b5 \u03c0\u03b1\u03cd\u03c3\u03b7", "pd": "\u0391\u03bd\u03af\u03c7\u03bd\u03b5\u03c5\u03c3\u03b7 \u03c4\u03c3\u03b9\u03bc\u03c0\u03ae\u03bc\u03b1\u03c4\u03bf\u03c2", + "pwrd": "\u0391\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7", + "pwru": "\u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7", "rdy": "\u0388\u03c4\u03bf\u03b9\u03bc\u03bf", "scf": "\u03a3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03b3\u03ac\u03c4\u03b1\u03c2 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03b5\u03ba\u03ba\u03af\u03bd\u03b7\u03c3\u03b7", "sdf": "\u0393\u03b5\u03bc\u03ac\u03c4\u03bf \u03c3\u03c5\u03c1\u03c4\u03ac\u03c1\u03b9 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03b5\u03ba\u03ba\u03af\u03bd\u03b7\u03c3\u03b7", diff --git a/homeassistant/components/litterrobot/translations/sensor.en.json b/homeassistant/components/litterrobot/translations/sensor.en.json index 5491a6a835f..65cf08e980e 100644 --- a/homeassistant/components/litterrobot/translations/sensor.en.json +++ b/homeassistant/components/litterrobot/translations/sensor.en.json @@ -4,6 +4,7 @@ "br": "Bonnet Removed", "ccc": "Clean Cycle Complete", "ccp": "Clean Cycle In Progress", + "cd": "Cat Detected", "csf": "Cat Sensor Fault", "csi": "Cat Sensor Interrupted", "cst": "Cat Sensor Timing", @@ -19,6 +20,8 @@ "otf": "Over Torque Fault", "p": "Paused", "pd": "Pinch Detect", + "pwrd": "Powering Down", + "pwru": "Powering Up", "rdy": "Ready", "scf": "Cat Sensor Fault At Startup", "sdf": "Drawer Full At Startup", diff --git a/homeassistant/components/litterrobot/translations/sensor.es.json b/homeassistant/components/litterrobot/translations/sensor.es.json index a67a15c6820..b954fe89f54 100644 --- a/homeassistant/components/litterrobot/translations/sensor.es.json +++ b/homeassistant/components/litterrobot/translations/sensor.es.json @@ -4,6 +4,7 @@ "br": "Bolsa extra\u00edda", "ccc": "Ciclo de limpieza completado", "ccp": "Ciclo de limpieza en curso", + "cd": "Gato detectado", "csf": "Fallo del sensor de gatos", "csi": "Sensor de gatos interrumpido", "cst": "Sincronizaci\u00f3n del sensor de gatos", @@ -19,6 +20,8 @@ "otf": "Fallo de par excesivo", "p": "En pausa", "pd": "Detecci\u00f3n de pellizcos", + "pwrd": "Apagando", + "pwru": "Encendiendo", "rdy": "Listo", "scf": "Fallo del sensor de gatos al inicio", "sdf": "Caj\u00f3n lleno al inicio", diff --git a/homeassistant/components/litterrobot/translations/sensor.et.json b/homeassistant/components/litterrobot/translations/sensor.et.json index 98b04dfd207..ea2399dd0e2 100644 --- a/homeassistant/components/litterrobot/translations/sensor.et.json +++ b/homeassistant/components/litterrobot/translations/sensor.et.json @@ -4,6 +4,7 @@ "br": "Kaast eemaldatud", "ccc": "Puhastusts\u00fckkel on l\u00f5ppenud", "ccp": "Puhastusts\u00fckkel on pooleli", + "cd": "Kassi on m\u00e4rgatud", "csf": "Kassianduri viga", "csi": "Kassianduri h\u00e4iring", "cst": "Kassianduri ajastus", @@ -19,6 +20,8 @@ "otf": "\u00dclekoornuse viga", "p": "Ootel", "pd": "Pinch Detect", + "pwrd": "Sulgumine", + "pwru": "K\u00e4ivitumine", "rdy": "Valmis", "scf": "Kassianduri viga k\u00e4ivitamisel", "sdf": "Sahtel t\u00e4is k\u00e4ivitamisel", diff --git a/homeassistant/components/litterrobot/translations/sensor.fr.json b/homeassistant/components/litterrobot/translations/sensor.fr.json index fba0796e2b0..ab21dd9b3e4 100644 --- a/homeassistant/components/litterrobot/translations/sensor.fr.json +++ b/homeassistant/components/litterrobot/translations/sensor.fr.json @@ -4,6 +4,7 @@ "br": "Capot retir\u00e9", "ccc": "Cycle de nettoyage termin\u00e9", "ccp": "Cycle de nettoyage en cours", + "cd": "Chat d\u00e9tect\u00e9", "csf": "D\u00e9faut du capteur de chat", "csi": "Interruption du capteur de chat", "cst": "Minutage du capteur de chat", @@ -19,6 +20,8 @@ "otf": "D\u00e9faut de surcouple", "p": "En pause", "pd": "D\u00e9tection de pincement", + "pwrd": "Mise hors tension", + "pwru": "Mise sous tension", "rdy": "Pr\u00eat", "scf": "D\u00e9faut du capteur de chat au d\u00e9marrage", "sdf": "Tiroir plein au d\u00e9marrage", diff --git a/homeassistant/components/litterrobot/translations/sensor.hu.json b/homeassistant/components/litterrobot/translations/sensor.hu.json index 6c68a231bf4..85709bd209d 100644 --- a/homeassistant/components/litterrobot/translations/sensor.hu.json +++ b/homeassistant/components/litterrobot/translations/sensor.hu.json @@ -4,6 +4,7 @@ "br": "Tet\u0151 elt\u00e1vol\u00edtva", "ccc": "Tiszt\u00edt\u00e1s befejez\u0151d\u00f6tt", "ccp": "Tiszt\u00edt\u00e1s folyamatban", + "cd": "Macska \u00e9szlelve", "csf": "Macska\u00e9rz\u00e9kel\u0151 hiba", "csi": "Macska\u00e9rz\u00e9kel\u0151 megszak\u00edtva", "cst": "Macska\u00e9rz\u00e9kel\u0151 id\u0151z\u00edt\u00e9se", @@ -19,6 +20,8 @@ "otf": "T\u00falzott nyomat\u00e9k hiba", "p": "Sz\u00fcnetel", "pd": "Pinch Detect", + "pwrd": "Kikapcsol\u00e1s", + "pwru": "Bekapcsol\u00e1s", "rdy": "K\u00e9sz", "scf": "Macska\u00e9rz\u00e9kel\u0151 hiba ind\u00edt\u00e1skor", "sdf": "Ind\u00edt\u00e1skor megtelt a fi\u00f3k", diff --git a/homeassistant/components/litterrobot/translations/sensor.id.json b/homeassistant/components/litterrobot/translations/sensor.id.json index 20f76ca4322..7a2382a4aef 100644 --- a/homeassistant/components/litterrobot/translations/sensor.id.json +++ b/homeassistant/components/litterrobot/translations/sensor.id.json @@ -2,23 +2,30 @@ "state": { "litterrobot__status_code": { "br": "Bonnet Dihapus", - "ccc": "Siklus Bersih Selesai", - "ccp": "Siklus Bersih Sedang Berlangsung", + "ccc": "Siklus Pembersihan Selesai", + "ccp": "Siklus Pembersihan Sedang Berlangsung", + "cd": "Kucing Terdeteksi", "csf": "Kesalahan Sensor Kucing", "csi": "Sensor Kucing Terganggu", "cst": "Waktu Sensor Kucing", "df1": "Laci Hampir Penuh - Tersisa 2 Siklus", "df2": "Laci Hampir Penuh - Tersisa 1 Siklus", "dfs": "Laci Penuh", - "ec": "Siklus Kosong", - "hpf": "Kesalahan Posisi Rumah", + "dhf": "Kesalahan Posisi Dump + Home", + "dpf": "Kesalahan Posisi Dump", + "ec": "Siklus Pengosongan", + "hpf": "Kesalahan Posisi Home", "off": "Mati", "offline": "Luring", "otf": "Kesalahan Torsi Berlebih", "p": "Jeda", + "pd": "Deteksi Pinch", + "pwrd": "Mematikan Daya", + "pwru": "Menyalakan", "rdy": "Siap", "scf": "Kesalahan Sensor Kucing Saat Mulai", - "sdf": "Laci Penuh Saat Memulai" + "sdf": "Laci Penuh Saat Memulai", + "spf": "Deteksi Pinch Saat Memulai" } } } \ No newline at end of file diff --git a/homeassistant/components/litterrobot/translations/sensor.it.json b/homeassistant/components/litterrobot/translations/sensor.it.json index 926cb5d68d7..c578c2aa9cd 100644 --- a/homeassistant/components/litterrobot/translations/sensor.it.json +++ b/homeassistant/components/litterrobot/translations/sensor.it.json @@ -4,6 +4,7 @@ "br": "Coperchio rimosso", "ccc": "Ciclo di pulizia completato", "ccp": "Ciclo di pulizia in corso", + "cd": "Gatto rilevato", "csf": "Errore del sensore Gatto", "csi": "Sensore Gatto interrotto", "cst": "Temporizzazione del sensore Gatto", @@ -19,6 +20,8 @@ "otf": "Errore di sovracoppia", "p": "In pausa", "pd": "Antipresa", + "pwrd": "Spegnimento", + "pwru": "Accensione", "rdy": "Pronto", "scf": "Errore del sensore Gatto all'avvio", "sdf": "Cassetto pieno all'avvio", diff --git a/homeassistant/components/litterrobot/translations/sensor.nl.json b/homeassistant/components/litterrobot/translations/sensor.nl.json index 6a383627aee..cf0d1537cdf 100644 --- a/homeassistant/components/litterrobot/translations/sensor.nl.json +++ b/homeassistant/components/litterrobot/translations/sensor.nl.json @@ -4,6 +4,7 @@ "br": "Motorkap verwijderd", "ccc": "Reinigingscyclus voltooid", "ccp": "Reinigingscyclus in uitvoering", + "cd": "Kat gedetecteerd", "csf": "Kattensensor fout", "csi": "Kattensensor onderbroken", "cst": "Timing kattensensor", @@ -17,6 +18,8 @@ "off": "Uit", "offline": "Offline", "p": "Gepauzeerd", + "pwrd": "Uitschakelen", + "pwru": "Opstarten", "rdy": "Gereed", "scf": "Kattensensorfout bij opstarten", "sdf": "Lade vol bij opstarten" diff --git a/homeassistant/components/litterrobot/translations/sensor.no.json b/homeassistant/components/litterrobot/translations/sensor.no.json index 2997930e53b..f1a75e6902b 100644 --- a/homeassistant/components/litterrobot/translations/sensor.no.json +++ b/homeassistant/components/litterrobot/translations/sensor.no.json @@ -4,6 +4,7 @@ "br": "Panser fjernet", "ccc": "Rengj\u00f8ringssyklus fullf\u00f8rt", "ccp": "Rengj\u00f8ringssyklus p\u00e5g\u00e5r", + "cd": "Katt oppdaget", "csf": "Feil p\u00e5 kattesensor", "csi": "Kattesensor avbrutt", "cst": "Tidsberegning for kattesensor", @@ -19,6 +20,8 @@ "otf": "Over dreiemomentfeil", "p": "Pauset", "pd": "Knip gjenkjenning", + "pwrd": "Sl\u00e5r av", + "pwru": "Sl\u00e5r p\u00e5", "rdy": "Klar", "scf": "Kattesensorfeil ved oppstart", "sdf": "Skuff full ved oppstart", diff --git a/homeassistant/components/litterrobot/translations/sensor.pl.json b/homeassistant/components/litterrobot/translations/sensor.pl.json index b3063c7ca62..7c70072307e 100644 --- a/homeassistant/components/litterrobot/translations/sensor.pl.json +++ b/homeassistant/components/litterrobot/translations/sensor.pl.json @@ -4,6 +4,7 @@ "br": "pokrywa otwarta", "ccc": "cykl czyszczenia zako\u0144czony", "ccp": "cykl czyszczenia w toku", + "cd": "wykryto kota", "csf": "b\u0142\u0105d sensora obecno\u015bci", "csi": "przerwa w pracy sensora", "cst": "czas pracy sensora", @@ -19,6 +20,8 @@ "otf": "b\u0142\u0105d nadmiernego momentu obrotowego", "p": "wstrzymany", "pd": "detektor obecno\u015bci", + "pwrd": "wy\u0142\u0105czanie", + "pwru": "w\u0142\u0105czanie", "rdy": "gotowy", "scf": "b\u0142\u0105d sensora obecno\u015bci podczas uruchamiania", "sdf": "szuflada pe\u0142na podczas uruchamiania", diff --git a/homeassistant/components/litterrobot/translations/sensor.pt-BR.json b/homeassistant/components/litterrobot/translations/sensor.pt-BR.json index 9eb1dd8be8b..cf949118f54 100644 --- a/homeassistant/components/litterrobot/translations/sensor.pt-BR.json +++ b/homeassistant/components/litterrobot/translations/sensor.pt-BR.json @@ -4,6 +4,7 @@ "br": "Cap\u00f4 removido", "ccc": "Ciclo de limpeza conclu\u00eddo", "ccp": "Ciclo de limpeza em andamento", + "cd": "Gato detectado", "csf": "Falha do Sensor Cat", "csi": "Sensor Cat interrompido", "cst": "Sincroniza\u00e7\u00e3o do Sensor Cat", @@ -19,6 +20,8 @@ "otf": "Falha de sobretorque", "p": "Pausado", "pd": "Detec\u00e7\u00e3o de pin\u00e7a", + "pwrd": "Desligando", + "pwru": "Ligando", "rdy": "Pronto", "scf": "Falha do sensor Cat na inicializa\u00e7\u00e3o", "sdf": "Gaveta cheia na inicializa\u00e7\u00e3o", diff --git a/homeassistant/components/litterrobot/translations/sensor.ru.json b/homeassistant/components/litterrobot/translations/sensor.ru.json index 3a28cfe08ec..dfc5d9a9b33 100644 --- a/homeassistant/components/litterrobot/translations/sensor.ru.json +++ b/homeassistant/components/litterrobot/translations/sensor.ru.json @@ -4,6 +4,7 @@ "br": "\u041a\u043e\u0436\u0443\u0445 \u0441\u043d\u044f\u0442", "ccc": "\u0426\u0438\u043a\u043b \u043e\u0447\u0438\u0441\u0442\u043a\u0438 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d", "ccp": "\u0412\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f \u0446\u0438\u043a\u043b \u043e\u0447\u0438\u0441\u0442\u043a\u0438", + "cd": "\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d \u043a\u043e\u0442", "csf": "\u041d\u0435\u0438\u0441\u043f\u0440\u0430\u0432\u043d\u043e\u0441\u0442\u044c \u0434\u0430\u0442\u0447\u0438\u043a\u0430", "csi": "\u0414\u0430\u0442\u0447\u0438\u043a \u043f\u0435\u0440\u0435\u043a\u0440\u044b\u0442", "cst": "\u0412\u0440\u0435\u043c\u044f \u0441\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u043d\u0438\u044f \u0434\u0430\u0442\u0447\u0438\u043a\u0430", @@ -19,6 +20,8 @@ "otf": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0435\u0432\u044b\u0448\u0435\u043d\u0438\u044f \u043a\u0440\u0443\u0442\u044f\u0449\u0435\u0433\u043e \u043c\u043e\u043c\u0435\u043d\u0442\u0430", "p": "\u041f\u0430\u0443\u0437\u0430", "pd": "\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u0435 \u0437\u0430\u0449\u0435\u043c\u043b\u0435\u043d\u0438\u044f", + "pwrd": "\u041e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043f\u0438\u0442\u0430\u043d\u0438\u044f", + "pwru": "\u0412\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043f\u0438\u0442\u0430\u043d\u0438\u044f", "rdy": "\u0413\u043e\u0442\u043e\u0432", "scf": "\u041d\u0435\u0438\u0441\u043f\u0440\u0430\u0432\u043d\u043e\u0441\u0442\u044c \u0434\u0430\u0442\u0447\u0438\u043a\u0430 \u043f\u0440\u0438 \u0437\u0430\u043f\u0443\u0441\u043a\u0435", "sdf": "\u042f\u0449\u0438\u043a \u043f\u043e\u043b\u043d\u044b\u0439 \u043f\u0440\u0438 \u0437\u0430\u043f\u0443\u0441\u043a\u0435", diff --git a/homeassistant/components/litterrobot/translations/sensor.zh-Hant.json b/homeassistant/components/litterrobot/translations/sensor.zh-Hant.json index f51c03f9c8c..e070bb69f51 100644 --- a/homeassistant/components/litterrobot/translations/sensor.zh-Hant.json +++ b/homeassistant/components/litterrobot/translations/sensor.zh-Hant.json @@ -4,6 +4,7 @@ "br": "\u4e0a\u84cb\u906d\u958b\u555f", "ccc": "\u8c93\u7802\u6e05\u7406\u5b8c\u6210", "ccp": "\u8c93\u7802\u6e05\u7406\u4e2d", + "cd": "\u5075\u6e2c\u5230\u8c93\u54aa", "csf": "\u8c93\u54aa\u611f\u6e2c\u5668\u6545\u969c", "csi": "\u8c93\u54aa\u611f\u6e2c\u5668\u906d\u4e2d\u65b7", "cst": "\u8c93\u54aa\u611f\u6e2c\u5668\u8a08\u6642", @@ -19,6 +20,8 @@ "otf": "\u8f49\u52d5\u5931\u6557", "p": "\u5df2\u66ab\u505c", "pd": "\u7570\u7269\u5075\u6e2c", + "pwrd": "\u95dc\u6a5f\u4e2d", + "pwru": "\u555f\u52d5\u4e2d", "rdy": "\u6e96\u5099\u5c31\u7dd2", "scf": "\u555f\u52d5\u6642\u8c93\u54aa\u611f\u6e2c\u5668\u5931\u6548", "sdf": "\u555f\u52d5\u6642\u6392\u5ee2\u76d2\u5df2\u6eff", diff --git a/homeassistant/components/litterrobot/translations/sv.json b/homeassistant/components/litterrobot/translations/sv.json index 939b543adea..e8919b760d8 100644 --- a/homeassistant/components/litterrobot/translations/sv.json +++ b/homeassistant/components/litterrobot/translations/sv.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Konto har redan konfigurerats" + "already_configured": "Konto har redan konfigurerats", + "reauth_successful": "\u00c5terautentisering lyckades" }, "error": { "cannot_connect": "Det gick inte att ansluta.", @@ -9,6 +10,13 @@ "unknown": "Ov\u00e4ntat fel" }, "step": { + "reauth_confirm": { + "data": { + "password": "L\u00f6senord" + }, + "description": "Uppdatera ditt l\u00f6senord f\u00f6r {username}", + "title": "\u00c5terautenticera integration" + }, "user": { "data": { "password": "L\u00f6senord", diff --git a/homeassistant/components/litterrobot/translations/tr.json b/homeassistant/components/litterrobot/translations/tr.json index a83e1936fb4..193413280eb 100644 --- a/homeassistant/components/litterrobot/translations/tr.json +++ b/homeassistant/components/litterrobot/translations/tr.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" }, "error": { "cannot_connect": "Ba\u011flanma hatas\u0131", @@ -9,6 +10,13 @@ "unknown": "Beklenmeyen hata" }, "step": { + "reauth_confirm": { + "data": { + "password": "Parola" + }, + "description": "L\u00fctfen {username} i\u00e7in \u015fifrenizi g\u00fcncelleyin", + "title": "Entegrasyonu Yeniden Do\u011frula" + }, "user": { "data": { "password": "Parola", diff --git a/homeassistant/components/litterrobot/translations/zh-Hant.json b/homeassistant/components/litterrobot/translations/zh-Hant.json index d83d49912ea..e3d32a2d75f 100644 --- a/homeassistant/components/litterrobot/translations/zh-Hant.json +++ b/homeassistant/components/litterrobot/translations/zh-Hant.json @@ -24,5 +24,11 @@ } } } + }, + "issues": { + "migrated_attributes": { + "description": "\u5438\u5875\u5668\u5be6\u9ad4\u5c6c\u6027\u73fe\u5728\u53ef\u4f5c\u70ba\u8a3a\u65b7\u8cc7\u6599\u611f\u6e2c\u5668\u3002\n\n\u5047\u5982\u6709\u81ea\u52d5\u5316\u6216\u8173\u672c\u4f7f\u7528\u5230\u9019\u4e9b\u5c6c\u6027\u3001\u8acb\u9032\u884c\u8abf\u6574\u3002", + "title": "Litter-Robot \u5c6c\u6027\u73fe\u5728\u6709\u7368\u7acb\u7684\u611f\u6e2c\u5668" + } } } \ No newline at end of file diff --git a/homeassistant/components/litterrobot/vacuum.py b/homeassistant/components/litterrobot/vacuum.py index 27cd3e6758a..55f0a182959 100644 --- a/homeassistant/components/litterrobot/vacuum.py +++ b/homeassistant/components/litterrobot/vacuum.py @@ -1,6 +1,7 @@ """Support for Litter-Robot "Vacuum".""" from __future__ import annotations +from datetime import time from typing import Any from pylitterbot import LitterRobot @@ -22,9 +23,10 @@ from homeassistant.const import STATE_OFF from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.entity_platform import AddEntitiesCallback +import homeassistant.util.dt as dt_util from .const import DOMAIN -from .entity import LitterRobotControlEntity, async_update_unique_id +from .entity import LitterRobotEntity, async_update_unique_id from .hub import LitterRobotHub SERVICE_SET_SLEEP_MODE = "set_sleep_mode" @@ -70,7 +72,7 @@ async def async_setup_entry( ) -class LitterRobotCleaner(LitterRobotControlEntity[LitterRobot], StateVacuumEntity): +class LitterRobotCleaner(LitterRobotEntity[LitterRobot], StateVacuumEntity): """Litter-Robot "Vacuum" Cleaner.""" _attr_supported_features = ( @@ -95,24 +97,41 @@ class LitterRobotCleaner(LitterRobotControlEntity[LitterRobot], StateVacuumEntit async def async_turn_on(self, **kwargs: Any) -> None: """Turn the cleaner on, starting a clean cycle.""" - await self.perform_action_and_refresh(self.robot.set_power_status, True) + await self.robot.set_power_status(True) async def async_turn_off(self, **kwargs: Any) -> None: """Turn the unit off, stopping any cleaning in progress as is.""" - await self.perform_action_and_refresh(self.robot.set_power_status, False) + await self.robot.set_power_status(False) async def async_start(self) -> None: """Start a clean cycle.""" - await self.perform_action_and_refresh(self.robot.start_cleaning) + await self.robot.start_cleaning() async def async_set_sleep_mode( self, enabled: bool, start_time: str | None = None ) -> None: """Set the sleep mode.""" - await self.perform_action_and_refresh( - self.robot.set_sleep_mode, - enabled, - self.parse_time_at_default_timezone(start_time), + await self.robot.set_sleep_mode( + enabled, self.parse_time_at_default_timezone(start_time) + ) + + @staticmethod + def parse_time_at_default_timezone(time_str: str | None) -> time | None: + """Parse a time string and add default timezone.""" + if time_str is None: + return None + + if (parsed_time := dt_util.parse_time(time_str)) is None: # pragma: no cover + return None + + return ( + dt_util.start_of_local_day() + .replace( + hour=parsed_time.hour, + minute=parsed_time.minute, + second=parsed_time.second, + ) + .timetz() ) @property diff --git a/homeassistant/components/locative/device_tracker.py b/homeassistant/components/locative/device_tracker.py index cd927aace38..f8fa1671034 100644 --- a/homeassistant/components/locative/device_tracker.py +++ b/homeassistant/components/locative/device_tracker.py @@ -64,13 +64,13 @@ class LocativeEntity(TrackerEntity): """Return the source type, eg gps or router, of the device.""" return SourceType.GPS - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register state update callback.""" self._unsub_dispatcher = async_dispatcher_connect( self.hass, TRACKER_UPDATE, self._async_receive_data ) - async def async_will_remove_from_hass(self): + async def async_will_remove_from_hass(self) -> None: """Clean up after entity before removal.""" self._unsub_dispatcher() diff --git a/homeassistant/components/locative/translations/pt.json b/homeassistant/components/locative/translations/pt.json index 93575068121..4666c1e91de 100644 --- a/homeassistant/components/locative/translations/pt.json +++ b/homeassistant/components/locative/translations/pt.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "N\u00e3o ligado ao Home Assistant Cloud.", "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel.", "webhook_not_internet_accessible": "O seu Home Assistant necessita estar acess\u00edvel a partir da Internet para receber mensagens do tipo webhook." }, diff --git a/homeassistant/components/lock/__init__.py b/homeassistant/components/lock/__init__.py index b94cd33a015..d241d57e128 100644 --- a/homeassistant/components/lock/__init__.py +++ b/homeassistant/components/lock/__init__.py @@ -60,10 +60,12 @@ SUPPORT_OPEN = 1 PROP_TO_ATTR = {"changed_by": ATTR_CHANGED_BY, "code_format": ATTR_CODE_FORMAT} +# mypy: disallow-any-generics + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Track states and offer events for locks.""" - component = hass.data[DOMAIN] = EntityComponent( + component = hass.data[DOMAIN] = EntityComponent[LockEntity]( _LOGGER, DOMAIN, hass, SCAN_INTERVAL ) @@ -84,13 +86,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry.""" - component: EntityComponent = hass.data[DOMAIN] + component: EntityComponent[LockEntity] = hass.data[DOMAIN] return await component.async_setup_entry(entry) async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - component: EntityComponent = hass.data[DOMAIN] + component: EntityComponent[LockEntity] = hass.data[DOMAIN] return await component.async_unload_entry(entry) diff --git a/homeassistant/components/lock/manifest.json b/homeassistant/components/lock/manifest.json index f93d2962ea3..0a786c05865 100644 --- a/homeassistant/components/lock/manifest.json +++ b/homeassistant/components/lock/manifest.json @@ -3,5 +3,6 @@ "name": "Lock", "documentation": "https://www.home-assistant.io/integrations/lock", "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "entity" } diff --git a/homeassistant/components/logbook/__init__.py b/homeassistant/components/logbook/__init__.py index 1abfcaba6ff..fb1b9d78b89 100644 --- a/homeassistant/components/logbook/__init__.py +++ b/homeassistant/components/logbook/__init__.py @@ -7,7 +7,7 @@ from typing import Any import voluptuous as vol from homeassistant.components import frontend -from homeassistant.components.recorder.const import DOMAIN as RECORDER_DOMAIN +from homeassistant.components.recorder import DOMAIN as RECORDER_DOMAIN from homeassistant.components.recorder.filters import ( extract_include_exclude_filter_conf, merge_include_exclude_filters, @@ -32,14 +32,17 @@ from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass from . import rest_api, websocket_api -from .const import ( +from .const import ( # noqa: F401 ATTR_MESSAGE, DOMAIN, LOGBOOK_ENTITIES_FILTER, + LOGBOOK_ENTRY_CONTEXT_ID, LOGBOOK_ENTRY_DOMAIN, LOGBOOK_ENTRY_ENTITY_ID, + LOGBOOK_ENTRY_ICON, LOGBOOK_ENTRY_MESSAGE, LOGBOOK_ENTRY_NAME, + LOGBOOK_ENTRY_SOURCE, LOGBOOK_FILTERS, ) from .models import LazyEventPartialState # noqa: F401 diff --git a/homeassistant/components/logbook/manifest.json b/homeassistant/components/logbook/manifest.json index 66c0348a2ac..5b8a8d4c2a3 100644 --- a/homeassistant/components/logbook/manifest.json +++ b/homeassistant/components/logbook/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/logbook", "dependencies": ["frontend", "http", "recorder"], "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "system" } diff --git a/homeassistant/components/logbook/websocket_api.py b/homeassistant/components/logbook/websocket_api.py index 3af87b26caa..d28666963c8 100644 --- a/homeassistant/components/logbook/websocket_api.py +++ b/homeassistant/components/logbook/websocket_api.py @@ -31,7 +31,6 @@ from .processor import EventProcessor MAX_PENDING_LOGBOOK_EVENTS = 2048 EVENT_COALESCE_TIME = 0.35 -MAX_RECORDER_WAIT = 10 # minimum size that we will split the query BIG_QUERY_HOURS = 25 # how many hours to deliver in the first chunk when we split the query @@ -48,6 +47,7 @@ class LogbookLiveStream: subscriptions: list[CALLBACK_TYPE] end_time_unsub: CALLBACK_TYPE | None = None task: asyncio.Task | None = None + wait_sync_task: asyncio.Task | None = None @callback @@ -57,18 +57,6 @@ def async_setup(hass: HomeAssistant) -> None: websocket_api.async_register_command(hass, ws_event_stream) -async def _async_wait_for_recorder_sync(hass: HomeAssistant) -> None: - """Wait for the recorder to sync.""" - try: - await asyncio.wait_for( - get_instance(hass).async_block_till_done(), MAX_RECORDER_WAIT - ) - except asyncio.TimeoutError: - _LOGGER.debug( - "Recorder is behind more than %s seconds, starting live stream; Some results may be missing" - ) - - @callback def _async_send_empty_response( connection: ActiveConnection, msg_id: int, start_time: dt, end_time: dt | None @@ -347,8 +335,11 @@ async def ws_event_stream( 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( @@ -395,43 +386,6 @@ async def ws_event_stream( partial=True, ) - await _async_wait_for_recorder_sync(hass) - if msg_id not in connection.subscriptions: - # Unsubscribe happened while waiting for recorder - return - - # - # Fetch any events from the database that have - # not been committed since the original fetch - # so we can switch over to using the subscriptions - # - # We only want events that happened after the last event - # we had from the last database query or the maximum - # time we allow the recorder to be behind - # - max_recorder_behind = subscriptions_setup_complete_time - timedelta( - seconds=MAX_RECORDER_WAIT - ) - second_fetch_start_time = max( - last_event_time or max_recorder_behind, max_recorder_behind - ) - await _async_send_historical_events( - hass, - connection, - msg_id, - second_fetch_start_time, - subscriptions_setup_complete_time, - messages.event_message, - event_processor, - partial=False, - ) - - if not subscriptions: - # Unsubscribe happened while waiting for formatted events - # or there are no supported entities (all UOM or state class) - # or devices - return - live_stream.task = asyncio.create_task( _async_events_consumer( subscriptions_setup_complete_time, @@ -442,6 +396,34 @@ 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() + ) + await live_stream.wait_sync_task + + # + # Fetch any events from the database that have + # not been committed since the original fetch + # so we can switch over to using the subscriptions + # + # We only want events that happened after the last event + # we had from the last database query + # + await _async_send_historical_events( + hass, + connection, + msg_id, + last_event_time or start_time, + subscriptions_setup_complete_time, + messages.event_message, + event_processor, + partial=False, + ) + def _ws_formatted_get_events( msg_id: int, diff --git a/homeassistant/components/logger/manifest.json b/homeassistant/components/logger/manifest.json index 2cb04538260..ef0a6fa2e65 100644 --- a/homeassistant/components/logger/manifest.json +++ b/homeassistant/components/logger/manifest.json @@ -3,5 +3,6 @@ "name": "Logger", "documentation": "https://www.home-assistant.io/integrations/logger", "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "system" } diff --git a/homeassistant/components/logi_circle/camera.py b/homeassistant/components/logi_circle/camera.py index 231de83a135..733e49ca0bf 100644 --- a/homeassistant/components/logi_circle/camera.py +++ b/homeassistant/components/logi_circle/camera.py @@ -75,7 +75,7 @@ class LogiCam(Camera): self._ffmpeg = ffmpeg self._listeners = [] - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Connect camera methods to signals.""" def _dispatch_proxy(method): @@ -111,7 +111,7 @@ class LogiCam(Camera): ] ) - async def async_will_remove_from_hass(self): + async def async_will_remove_from_hass(self) -> None: """Disconnect dispatcher listeners when removed.""" for detach in self._listeners: detach() @@ -161,11 +161,11 @@ class LogiCam(Camera): """Return a still image from the camera.""" return await self._camera.live_stream.download_jpeg() - async def async_turn_off(self): + async def async_turn_off(self) -> None: """Disable streaming mode for this camera.""" await self._camera.set_config("streaming", False) - async def async_turn_on(self): + async def async_turn_on(self) -> None: """Enable streaming mode for this camera.""" await self._camera.set_config("streaming", True) @@ -210,6 +210,6 @@ class LogiCam(Camera): filename=snapshot_file, refresh=True ) - async def async_update(self): + async def async_update(self) -> None: """Update camera entity and refresh attributes.""" await self._camera.update() diff --git a/homeassistant/components/logi_circle/sensor.py b/homeassistant/components/logi_circle/sensor.py index da115f789da..baf6d933916 100644 --- a/homeassistant/components/logi_circle/sensor.py +++ b/homeassistant/components/logi_circle/sensor.py @@ -112,7 +112,7 @@ class LogiSensor(SensorEntity): ) return self.entity_description.icon - async def async_update(self): + async def async_update(self) -> None: """Get the latest data and updates the state.""" _LOGGER.debug("Pulling data from %s sensor", self.name) await self._camera.update() diff --git a/homeassistant/components/logi_circle/translations/fr.json b/homeassistant/components/logi_circle/translations/fr.json index 5c4bb6fab9d..94c48c6fdd6 100644 --- a/homeassistant/components/logi_circle/translations/fr.json +++ b/homeassistant/components/logi_circle/translations/fr.json @@ -8,7 +8,7 @@ }, "error": { "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification expir\u00e9.", - "follow_link": "Veuillez suivre le lien et vous authentifier avant d'appuyer sur Soumettre.", + "follow_link": "Veuillez suivre le lien et vous authentifier avant d'appuyer sur \u00ab\u00a0Valider\u00a0\u00bb.", "invalid_auth": "Authentification non valide" }, "step": { diff --git a/homeassistant/components/london_air/sensor.py b/homeassistant/components/london_air/sensor.py index 23bf3bb2e64..33fe1d4d7fb 100644 --- a/homeassistant/components/london_air/sensor.py +++ b/homeassistant/components/london_air/sensor.py @@ -141,7 +141,7 @@ class AirSensor(SensorEntity): attrs["data"] = self._site_data return attrs - def update(self): + def update(self) -> None: """Update the sensor.""" sites_status = [] self._api_data.update() diff --git a/homeassistant/components/lookin/climate.py b/homeassistant/components/lookin/climate.py index b8042cef72a..5b3ecefa4ff 100644 --- a/homeassistant/components/lookin/climate.py +++ b/homeassistant/components/lookin/climate.py @@ -7,8 +7,7 @@ from typing import Any, Final, cast from aiolookin import Climate, MeteoSensor from aiolookin.models import UDPCommandType, UDPEvent -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( ATTR_HVAC_MODE, FAN_AUTO, FAN_HIGH, @@ -16,6 +15,7 @@ from homeassistant.components.climate.const import ( FAN_MIDDLE, SWING_BOTH, SWING_OFF, + ClimateEntity, ClimateEntityFeature, HVACMode, ) diff --git a/homeassistant/components/lookin/media_player.py b/homeassistant/components/lookin/media_player.py index fa3a3622a31..f0e9c7e5928 100644 --- a/homeassistant/components/lookin/media_player.py +++ b/homeassistant/components/lookin/media_player.py @@ -9,9 +9,10 @@ from homeassistant.components.media_player import ( MediaPlayerDeviceClass, MediaPlayerEntity, MediaPlayerEntityFeature, + MediaPlayerState, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import STATE_ON, STATE_STANDBY, Platform +from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -112,13 +113,13 @@ class LookinMedia(LookinPowerPushRemoteEntity, MediaPlayerEntity): async def async_turn_off(self) -> None: """Turn the media player off.""" await self._async_send_command(self._power_off_command) - self._attr_state = STATE_STANDBY + self._attr_state = MediaPlayerState.STANDBY self.async_write_ha_state() async def async_turn_on(self) -> None: """Turn the media player on.""" await self._async_send_command(self._power_on_command) - self._attr_state = STATE_ON + self._attr_state = MediaPlayerState.ON self.async_write_ha_state() def _update_from_status(self, status: str) -> None: @@ -135,5 +136,7 @@ class LookinMedia(LookinPowerPushRemoteEntity, MediaPlayerEntity): state = status[0] mute = status[2] - self._attr_state = STATE_ON if state == "1" else STATE_STANDBY + self._attr_state = ( + MediaPlayerState.ON if state == "1" else MediaPlayerState.STANDBY + ) self._attr_is_volume_muted = mute == "0" diff --git a/homeassistant/components/lookin/translations/cs.json b/homeassistant/components/lookin/translations/cs.json index 50dcaf1b95f..9193ce36658 100644 --- a/homeassistant/components/lookin/translations/cs.json +++ b/homeassistant/components/lookin/translations/cs.json @@ -1,9 +1,14 @@ { "config": { "abort": { - "already_in_progress": "Konfigurace ji\u017e prob\u00edh\u00e1" + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "already_in_progress": "Konfigurace ji\u017e prob\u00edh\u00e1", + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "no_devices_found": "V s\u00edti nebyla nalezena \u017e\u00e1dn\u00e1 za\u0159\u00edzen\u00ed" }, "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "no_devices_found": "V s\u00edti nebyla nalezena \u017e\u00e1dn\u00e1 za\u0159\u00edzen\u00ed", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, "step": { @@ -11,6 +16,11 @@ "data": { "name": "Jm\u00e9no" } + }, + "user": { + "data": { + "ip_address": "IP adresa" + } } } } diff --git a/homeassistant/components/lovelace/__init__.py b/homeassistant/components/lovelace/__init__.py index cf74a3a588c..c2e709aeb40 100644 --- a/homeassistant/components/lovelace/__init__.py +++ b/homeassistant/components/lovelace/__init__.py @@ -14,7 +14,7 @@ from homeassistant.helpers.typing import ConfigType from homeassistant.loader import async_get_integration from . import dashboard, resources, websocket -from .const import ( +from .const import ( # noqa: F401 CONF_ICON, CONF_REQUIRE_ADMIN, CONF_SHOW_IN_SIDEBAR, @@ -23,6 +23,7 @@ from .const import ( DASHBOARD_BASE_CREATE_FIELDS, DEFAULT_ICON, DOMAIN, + EVENT_LOVELACE_UPDATED, MODE_STORAGE, MODE_YAML, RESOURCE_CREATE_FIELDS, diff --git a/homeassistant/components/lovelace/cast.py b/homeassistant/components/lovelace/cast.py index bd06d142bd3..02f5d0c0478 100644 --- a/homeassistant/components/lovelace/cast.py +++ b/homeassistant/components/lovelace/cast.py @@ -5,15 +5,19 @@ from __future__ import annotations from pychromecast import Chromecast from pychromecast.const import CAST_TYPE_CHROMECAST -from homeassistant.components.cast.const import DOMAIN as CAST_DOMAIN +from homeassistant.components.cast import DOMAIN as CAST_DOMAIN from homeassistant.components.cast.home_assistant_cast import ( ATTR_URL_PATH, ATTR_VIEW_PATH, NO_URL_AVAILABLE_ERROR, SERVICE_SHOW_VIEW, ) -from homeassistant.components.media_player import BrowseError, BrowseMedia -from homeassistant.components.media_player.const import MEDIA_CLASS_APP +from homeassistant.components.media_player import ( + BrowseError, + BrowseMedia, + MediaClass, + MediaType, +) from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError @@ -34,7 +38,7 @@ async def async_get_media_browser_root_object( return [ BrowseMedia( title="Dashboards", - media_class=MEDIA_CLASS_APP, + media_class=MediaClass.APP, media_content_id="", media_content_type=DOMAIN, thumbnail="https://brands.home-assistant.io/_/lovelace/logo.png", @@ -46,7 +50,7 @@ async def async_get_media_browser_root_object( async def async_browse_media( hass: HomeAssistant, - media_content_type: str, + media_content_type: MediaType | str, media_content_id: str, cast_type: str, ) -> BrowseMedia | None: @@ -64,7 +68,7 @@ async def async_browse_media( children = [ BrowseMedia( title="Default", - media_class=MEDIA_CLASS_APP, + media_class=MediaClass.APP, media_content_id=DEFAULT_DASHBOARD, media_content_type=DOMAIN, thumbnail="https://brands.home-assistant.io/_/lovelace/logo.png", @@ -96,7 +100,7 @@ async def async_browse_media( children.append( BrowseMedia( title=view["title"], - media_class=MEDIA_CLASS_APP, + media_class=MediaClass.APP, media_content_id=f'{info["url_path"]}/{view["path"]}', media_content_type=DOMAIN, thumbnail="https://brands.home-assistant.io/_/lovelace/logo.png", @@ -114,7 +118,7 @@ async def async_play_media( hass: HomeAssistant, cast_entity_id: str, chromecast: Chromecast, - media_type: str, + media_type: MediaType | str, media_id: str, ) -> bool: """Play media.""" @@ -195,7 +199,7 @@ def _item_from_info(info: dict) -> BrowseMedia: """Convert dashboard info to browse item.""" return BrowseMedia( title=info["title"], - media_class=MEDIA_CLASS_APP, + media_class=MediaClass.APP, media_content_id=info["url_path"], media_content_type=DOMAIN, thumbnail="https://brands.home-assistant.io/_/lovelace/logo.png", diff --git a/homeassistant/components/lovelace/manifest.json b/homeassistant/components/lovelace/manifest.json index 6f91a61b08c..7d9561f9755 100644 --- a/homeassistant/components/lovelace/manifest.json +++ b/homeassistant/components/lovelace/manifest.json @@ -2,5 +2,7 @@ "domain": "lovelace", "name": "Dashboards", "documentation": "https://www.home-assistant.io/integrations/lovelace", - "codeowners": ["@home-assistant/frontend"] + "codeowners": ["@home-assistant/frontend"], + "quality_scale": "internal", + "integration_type": "system" } diff --git a/homeassistant/components/luci/device_tracker.py b/homeassistant/components/luci/device_tracker.py index c57e235a8d2..82fc51ba5cd 100644 --- a/homeassistant/components/luci/device_tracker.py +++ b/homeassistant/components/luci/device_tracker.py @@ -38,7 +38,7 @@ PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( ) -def get_scanner(hass: HomeAssistant, config: ConfigType) -> DeviceScanner | None: +def get_scanner(hass: HomeAssistant, config: ConfigType) -> LuciDeviceScanner | None: """Validate the configuration and return a Luci scanner.""" scanner = LuciDeviceScanner(config[DOMAIN]) diff --git a/homeassistant/components/lupusec/switch.py b/homeassistant/components/lupusec/switch.py index 4b5f38a81b3..546f96fd0a6 100644 --- a/homeassistant/components/lupusec/switch.py +++ b/homeassistant/components/lupusec/switch.py @@ -2,6 +2,7 @@ from __future__ import annotations from datetime import timedelta +from typing import Any import lupupy.constants as CONST @@ -39,11 +40,11 @@ def setup_platform( class LupusecSwitch(LupusecDevice, SwitchEntity): """Representation of a Lupusec switch.""" - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn on the device.""" self._device.switch_on() - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn off the device.""" self._device.switch_off() diff --git a/homeassistant/components/lutron/__init__.py b/homeassistant/components/lutron/__init__.py index 75561dd275b..d8ccce8a6bc 100644 --- a/homeassistant/components/lutron/__init__.py +++ b/homeassistant/components/lutron/__init__.py @@ -124,7 +124,7 @@ class LutronDevice(Entity): self._controller = controller self._area_name = area_name - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register callbacks.""" self._lutron_device.subscribe(self._update_callback, None) @@ -133,7 +133,7 @@ class LutronDevice(Entity): self.schedule_update_ha_state() @property - def name(self): + def name(self) -> str: """Return the name of the device.""" return f"{self._area_name} {self._lutron_device.name}" diff --git a/homeassistant/components/lutron/scene.py b/homeassistant/components/lutron/scene.py index 68d1a4805fc..f2d008a1187 100644 --- a/homeassistant/components/lutron/scene.py +++ b/homeassistant/components/lutron/scene.py @@ -43,6 +43,6 @@ class LutronScene(LutronDevice, Scene): self._lutron_device.press() @property - def name(self): + def name(self) -> str: """Return the name of the device.""" return f"{self._area_name} {self._keypad_name}: {self._lutron_device.name}" diff --git a/homeassistant/components/lutron/switch.py b/homeassistant/components/lutron/switch.py index 49d4181a8e0..8595f809035 100644 --- a/homeassistant/components/lutron/switch.py +++ b/homeassistant/components/lutron/switch.py @@ -1,6 +1,8 @@ """Support for Lutron switches.""" from __future__ import annotations +from typing import Any + from homeassistant.components.switch import SwitchEntity from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -43,11 +45,11 @@ class LutronSwitch(LutronDevice, SwitchEntity): self._prev_state = None super().__init__(area_name, lutron_device, controller) - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" self._lutron_device.level = 100 - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the switch off.""" self._lutron_device.level = 0 @@ -61,7 +63,7 @@ class LutronSwitch(LutronDevice, SwitchEntity): """Return true if device is on.""" return self._lutron_device.last_level() > 0 - def update(self): + def update(self) -> None: """Call when forcing a refresh of the device.""" if self._prev_state is None: self._prev_state = self._lutron_device.level > 0 @@ -76,11 +78,11 @@ class LutronLed(LutronDevice, SwitchEntity): self._scene_name = scene_device.name super().__init__(area_name, led_device, controller) - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the LED on.""" self._lutron_device.state = 1 - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the LED off.""" self._lutron_device.state = 0 @@ -99,11 +101,11 @@ class LutronLed(LutronDevice, SwitchEntity): return self._lutron_device.last_state @property - def name(self): + def name(self) -> str: """Return the name of the LED.""" return f"{self._area_name} {self._keypad_name}: {self._scene_name} LED" - def update(self): + def update(self) -> None: """Call when forcing a refresh of the device.""" if self._lutron_device.last_state is not None: return diff --git a/homeassistant/components/lutron_caseta/binary_sensor.py b/homeassistant/components/lutron_caseta/binary_sensor.py index 4b1c53d194b..20fc221cdef 100644 --- a/homeassistant/components/lutron_caseta/binary_sensor.py +++ b/homeassistant/components/lutron_caseta/binary_sensor.py @@ -61,7 +61,7 @@ class LutronOccupancySensor(LutronCasetaDevice, BinarySensorEntity): """Return the brightness of the light.""" return self._device["status"] == OCCUPANCY_GROUP_OCCUPIED - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register callbacks.""" self._smartbridge.add_occupancy_subscriber( self.device_id, self.async_write_ha_state diff --git a/homeassistant/components/lutron_caseta/device_trigger.py b/homeassistant/components/lutron_caseta/device_trigger.py index b355c3dcc3f..b9fe89edf7f 100644 --- a/homeassistant/components/lutron_caseta/device_trigger.py +++ b/homeassistant/components/lutron_caseta/device_trigger.py @@ -405,7 +405,8 @@ async def async_get_triggers( triggers = [] if not (device := get_button_device_by_dr_id(hass, device_id)): - raise InvalidDeviceAutomationConfig(f"Device not found: {device_id}") + # Check if device is a valid button device. Return empty if not. + return [] valid_buttons = DEVICE_TYPE_SUBTYPE_MAP_TO_LEAP.get( _lutron_model_to_device_type(device["model"], device["type"]), {} diff --git a/homeassistant/components/lutron_caseta/logbook.py b/homeassistant/components/lutron_caseta/logbook.py index bcca548f64b..7bf1b467ff6 100644 --- a/homeassistant/components/lutron_caseta/logbook.py +++ b/homeassistant/components/lutron_caseta/logbook.py @@ -3,10 +3,7 @@ from __future__ import annotations from collections.abc import Callable -from homeassistant.components.logbook.const import ( - LOGBOOK_ENTRY_MESSAGE, - LOGBOOK_ENTRY_NAME, -) +from homeassistant.components.logbook import LOGBOOK_ENTRY_MESSAGE, LOGBOOK_ENTRY_NAME from homeassistant.core import Event, HomeAssistant, callback from .const import ( diff --git a/homeassistant/components/lutron_caseta/manifest.json b/homeassistant/components/lutron_caseta/manifest.json index c80d0deb794..88849391e24 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.13.1"], + "requirements": ["pylutron-caseta==0.15.2"], "config_flow": true, "zeroconf": ["_leap._tcp.local."], "homekit": { diff --git a/homeassistant/components/lutron_caseta/switch.py b/homeassistant/components/lutron_caseta/switch.py index 062c8891672..92ec6b35f98 100644 --- a/homeassistant/components/lutron_caseta/switch.py +++ b/homeassistant/components/lutron_caseta/switch.py @@ -1,5 +1,7 @@ """Support for Lutron Caseta switches.""" +from typing import Any + from homeassistant.components.switch import DOMAIN, SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -33,15 +35,15 @@ async def async_setup_entry( class LutronCasetaLight(LutronCasetaDeviceUpdatableEntity, SwitchEntity): """Representation of a Lutron Caseta switch.""" - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" await self._smartbridge.turn_on(self.device_id) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the switch off.""" await self._smartbridge.turn_off(self.device_id) @property - def is_on(self): + def is_on(self) -> bool: """Return true if device is on.""" return self._device["current_state"] > 0 diff --git a/homeassistant/components/lw12wifi/light.py b/homeassistant/components/lw12wifi/light.py index 7814475b41f..9f520a79ae1 100644 --- a/homeassistant/components/lw12wifi/light.py +++ b/homeassistant/components/lw12wifi/light.py @@ -116,7 +116,7 @@ class LW12WiFi(LightEntity): """Return True if unable to access real state of the entity.""" return True - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Instruct the light to turn on.""" self._light.light_on() if ATTR_HS_COLOR in kwargs: @@ -124,7 +124,7 @@ class LW12WiFi(LightEntity): self._light.set_color(*self._rgb_color) self._effect = None if ATTR_BRIGHTNESS in kwargs: - self._brightness = kwargs.get(ATTR_BRIGHTNESS) + self._brightness = kwargs[ATTR_BRIGHTNESS] brightness = int(self._brightness / 255 * 100) self._light.set_light_option(lw12.LW12_LIGHT.BRIGHTNESS, brightness) if ATTR_EFFECT in kwargs: diff --git a/homeassistant/components/lyric/climate.py b/homeassistant/components/lyric/climate.py index 8353ae15b3c..ae4afa0b0c6 100644 --- a/homeassistant/components/lyric/climate.py +++ b/homeassistant/components/lyric/climate.py @@ -10,10 +10,11 @@ from aiolyric.objects.device import LyricDevice from aiolyric.objects.location import LyricLocation import voluptuous as vol -from homeassistant.components.climate import ClimateEntity, ClimateEntityDescription -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, + ClimateEntity, + ClimateEntityDescription, ClimateEntityFeature, HVACAction, HVACMode, diff --git a/homeassistant/components/lyric/translations/es.json b/homeassistant/components/lyric/translations/es.json index e4bd36ad952..2506018a178 100644 --- a/homeassistant/components/lyric/translations/es.json +++ b/homeassistant/components/lyric/translations/es.json @@ -3,7 +3,7 @@ "abort": { "authorize_url_timeout": "Se agot\u00f3 el tiempo de espera para generar la URL de autorizaci\u00f3n.", "missing_configuration": "El componente no est\u00e1 configurado. Por favor, sigue la documentaci\u00f3n.", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "create_entry": { "default": "Autenticado correctamente" @@ -13,7 +13,7 @@ "title": "Selecciona el m\u00e9todo de autenticaci\u00f3n" }, "reauth_confirm": { - "description": "La integraci\u00f3n de Lyric necesita volver a autenticar tu cuenta.", + "description": "La integraci\u00f3n Lyric necesita volver a autenticar tu cuenta.", "title": "Volver a autenticar la integraci\u00f3n" } } diff --git a/homeassistant/components/magicseaweed/sensor.py b/homeassistant/components/magicseaweed/sensor.py index 38559f607a4..b0b2e92ada5 100644 --- a/homeassistant/components/magicseaweed/sensor.py +++ b/homeassistant/components/magicseaweed/sensor.py @@ -157,7 +157,7 @@ class MagicSeaweedSensor(SensorEntity): """Return the unit system of this entity.""" return self._unit_system - def update(self): + def update(self) -> None: """Get the latest data from Magicseaweed and updates the states.""" self.data.update() if self.hour is None: diff --git a/homeassistant/components/mailbox/__init__.py b/homeassistant/components/mailbox/__init__.py index 89b646beee4..f97b2c5337b 100644 --- a/homeassistant/components/mailbox/__init__.py +++ b/homeassistant/components/mailbox/__init__.py @@ -6,6 +6,7 @@ from contextlib import suppress from datetime import timedelta from http import HTTPStatus import logging +from typing import Any, Final from aiohttp import web from aiohttp.web_exceptions import HTTPNotFound @@ -13,7 +14,7 @@ import async_timeout from homeassistant.components import frontend from homeassistant.components.http import HomeAssistantView -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import Event, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_per_platform, discovery from homeassistant.helpers.entity import Entity @@ -21,15 +22,13 @@ from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.setup import async_prepare_setup_platform -# mypy: allow-untyped-defs, no-check-untyped-defs - _LOGGER = logging.getLogger(__name__) -DOMAIN = "mailbox" +DOMAIN: Final = "mailbox" -EVENT = "mailbox_updated" -CONTENT_TYPE_MPEG = "audio/mpeg" -CONTENT_TYPE_NONE = "none" +EVENT: Final = "mailbox_updated" +CONTENT_TYPE_MPEG: Final = "audio/mpeg" +CONTENT_TYPE_NONE: Final = "none" SCAN_INTERVAL = timedelta(seconds=30) @@ -84,7 +83,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: mailboxes.append(mailbox) mailbox_entity = MailboxEntity(mailbox) - component = EntityComponent( + component = EntityComponent[MailboxEntity]( logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL ) await component.async_add_entities([mailbox_entity]) @@ -98,7 +97,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: if setup_tasks: await asyncio.wait(setup_tasks) - async def async_platform_discovered(platform, info): + async def async_platform_discovered( + platform: str, info: DiscoveryInfoType | None + ) -> None: """Handle for discovered platform.""" await async_setup_platform(platform, discovery_info=info) @@ -115,27 +116,27 @@ class MailboxEntity(Entity): self.mailbox = mailbox self.message_count = 0 - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Complete entity initialization.""" @callback - def _mailbox_updated(event): + def _mailbox_updated(event: Event) -> None: self.async_schedule_update_ha_state(True) self.hass.bus.async_listen(EVENT, _mailbox_updated) self.async_schedule_update_ha_state(True) @property - def state(self): + def state(self) -> str: """Return the state of the binary sensor.""" return str(self.message_count) @property - def name(self): + def name(self) -> str: """Return the name of the entity.""" return self.mailbox.name - async def async_update(self): + async def async_update(self) -> None: """Retrieve messages from platform.""" messages = await self.mailbox.async_get_messages() self.message_count = len(messages) @@ -144,40 +145,40 @@ class MailboxEntity(Entity): class Mailbox: """Represent a mailbox device.""" - def __init__(self, hass, name): + def __init__(self, hass: HomeAssistant, name: str) -> None: """Initialize mailbox object.""" self.hass = hass self.name = name @callback - def async_update(self): + def async_update(self) -> None: """Send event notification of updated mailbox.""" self.hass.bus.async_fire(EVENT) @property - def media_type(self): + def media_type(self) -> str: """Return the supported media type.""" raise NotImplementedError() @property - def can_delete(self): + def can_delete(self) -> bool: """Return if messages can be deleted.""" return False @property - def has_media(self): + def has_media(self) -> bool: """Return if messages have attached media files.""" return False - async def async_get_media(self, msgid): + async def async_get_media(self, msgid: str) -> bytes: """Return the media blob for the msgid.""" raise NotImplementedError() - async def async_get_messages(self): + async def async_get_messages(self) -> list[dict[str, Any]]: """Return a list of the current messages.""" raise NotImplementedError() - async def async_delete(self, msgid): + async def async_delete(self, msgid: str) -> bool: """Delete the specified messages.""" raise NotImplementedError() @@ -193,7 +194,7 @@ class MailboxView(HomeAssistantView): """Initialize a basic mailbox view.""" self.mailboxes = mailboxes - def get_mailbox(self, platform): + def get_mailbox(self, platform: str) -> Mailbox: """Retrieve the specified mailbox.""" for mailbox in self.mailboxes: if mailbox.name == platform: @@ -209,7 +210,7 @@ class MailboxPlatformsView(MailboxView): async def get(self, request: web.Request) -> web.Response: """Retrieve list of platforms.""" - platforms = [] + platforms: list[dict[str, Any]] = [] for mailbox in self.mailboxes: platforms.append( { @@ -227,7 +228,7 @@ class MailboxMessageView(MailboxView): url = "/api/mailbox/messages/{platform}" name = "api:mailbox:messages" - async def get(self, request, platform): + async def get(self, request: web.Request, platform: str) -> web.Response: """Retrieve messages.""" mailbox = self.get_mailbox(platform) messages = await mailbox.async_get_messages() @@ -240,7 +241,7 @@ class MailboxDeleteView(MailboxView): url = "/api/mailbox/delete/{platform}/{msgid}" name = "api:mailbox:delete" - async def delete(self, request, platform, msgid): + async def delete(self, request: web.Request, platform: str, msgid: str) -> None: """Delete items.""" mailbox = self.get_mailbox(platform) await mailbox.async_delete(msgid) @@ -252,7 +253,9 @@ class MailboxMediaView(MailboxView): url = r"/api/mailbox/media/{platform}/{msgid}" name = "api:asteriskmbox:media" - async def get(self, request, platform, msgid): + async def get( + self, request: web.Request, platform: str, msgid: str + ) -> web.Response: """Retrieve media.""" mailbox = self.get_mailbox(platform) diff --git a/homeassistant/components/mailbox/manifest.json b/homeassistant/components/mailbox/manifest.json index 9d8a1403332..8d080888985 100644 --- a/homeassistant/components/mailbox/manifest.json +++ b/homeassistant/components/mailbox/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/mailbox", "dependencies": ["http"], "codeowners": [], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "entity" } diff --git a/homeassistant/components/manual/alarm_control_panel.py b/homeassistant/components/manual/alarm_control_panel.py index 9a5d84f5997..d96ada6e139 100644 --- a/homeassistant/components/manual/alarm_control_panel.py +++ b/homeassistant/components/manual/alarm_control_panel.py @@ -10,9 +10,7 @@ from typing import Any import voluptuous as vol import homeassistant.components.alarm_control_panel as alarm -from homeassistant.components.alarm_control_panel.const import ( - AlarmControlPanelEntityFeature, -) +from homeassistant.components.alarm_control_panel import AlarmControlPanelEntityFeature from homeassistant.const import ( CONF_ARMING_TIME, CONF_CODE, diff --git a/homeassistant/components/maxcube/binary_sensor.py b/homeassistant/components/maxcube/binary_sensor.py index f674ec38d37..c6c9ba8ebb0 100644 --- a/homeassistant/components/maxcube/binary_sensor.py +++ b/homeassistant/components/maxcube/binary_sensor.py @@ -43,7 +43,7 @@ class MaxCubeBinarySensorBase(BinarySensorEntity): self._device = device self._room = handler.cube.room_by_id(device.room_id) - def update(self): + def update(self) -> None: """Get latest data from MAX! Cube.""" self._cubehandle.update() diff --git a/homeassistant/components/maxcube/climate.py b/homeassistant/components/maxcube/climate.py index c5d04ae599c..6361600518b 100644 --- a/homeassistant/components/maxcube/climate.py +++ b/homeassistant/components/maxcube/climate.py @@ -3,6 +3,7 @@ from __future__ import annotations import logging import socket +from typing import Any from maxcube.device import ( MAX_DEVICE_MODE_AUTOMATIC, @@ -11,13 +12,13 @@ from maxcube.device import ( MAX_DEVICE_MODE_VACATION, ) -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( PRESET_AWAY, PRESET_BOOST, PRESET_COMFORT, PRESET_ECO, PRESET_NONE, + ClimateEntity, ClimateEntityFeature, HVACAction, HVACMode, @@ -183,7 +184,7 @@ class MaxCubeClimate(ClimateEntity): return None return temp - def set_temperature(self, **kwargs): + def set_temperature(self, **kwargs: Any) -> None: """Set new target temperatures.""" if (temp := kwargs.get(ATTR_TEMPERATURE)) is None: raise ValueError( @@ -207,7 +208,7 @@ class MaxCubeClimate(ClimateEntity): return PRESET_AWAY return PRESET_NONE - def set_preset_mode(self, preset_mode): + def set_preset_mode(self, preset_mode: str) -> None: """Set new operation mode.""" if preset_mode == PRESET_COMFORT: self._set_target(MAX_DEVICE_MODE_MANUAL, self._device.comfort_temperature) @@ -231,6 +232,6 @@ class MaxCubeClimate(ClimateEntity): return {} return {ATTR_VALVE_POSITION: self._device.valve_position} - def update(self): + def update(self) -> None: """Get latest data from MAX! Cube.""" self._cubehandle.update() diff --git a/homeassistant/components/mazda/switch.py b/homeassistant/components/mazda/switch.py index 844585720d7..7097237bc5d 100644 --- a/homeassistant/components/mazda/switch.py +++ b/homeassistant/components/mazda/switch.py @@ -1,4 +1,6 @@ """Platform for Mazda switch integration.""" +from typing import Any + from pymazda import Client as MazdaAPIClient from homeassistant.components.switch import SwitchEntity @@ -57,13 +59,13 @@ class MazdaChargingSwitch(MazdaEntity, SwitchEntity): self.async_write_ha_state() - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Start charging the vehicle.""" await self.client.start_charging(self.vehicle_id) await self.refresh_status_and_write_state() - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Stop charging the vehicle.""" await self.client.stop_charging(self.vehicle_id) diff --git a/homeassistant/components/mazda/translations/es.json b/homeassistant/components/mazda/translations/es.json index 53dec475cad..3702a1b8302 100644 --- a/homeassistant/components/mazda/translations/es.json +++ b/homeassistant/components/mazda/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "error": { "account_locked": "Cuenta bloqueada. Por favor, vuelve a intentarlo m\u00e1s tarde.", diff --git a/homeassistant/components/meater/sensor.py b/homeassistant/components/meater/sensor.py index a2753a42307..8322b9a0202 100644 --- a/homeassistant/components/meater/sensor.py +++ b/homeassistant/components/meater/sensor.py @@ -215,7 +215,7 @@ class MeaterProbeTemperature( return self.entity_description.value(device) @property - def available(self): + def available(self) -> bool: """Return if entity is available.""" # See if the device was returned from the API. If not, it's offline return ( diff --git a/homeassistant/components/meater/translations/cs.json b/homeassistant/components/meater/translations/cs.json index 72c98504526..2782d56e4b4 100644 --- a/homeassistant/components/meater/translations/cs.json +++ b/homeassistant/components/meater/translations/cs.json @@ -5,6 +5,11 @@ "unknown_auth_error": "Neo\u010dek\u00e1van\u00e1 chyba" }, "step": { + "reauth_confirm": { + "data": { + "password": "Heslo" + } + }, "user": { "data": { "password": "Heslo", diff --git a/homeassistant/components/meater/translations/es.json b/homeassistant/components/meater/translations/es.json index 1fc556b8ee2..c2524a3e47c 100644 --- a/homeassistant/components/meater/translations/es.json +++ b/homeassistant/components/meater/translations/es.json @@ -10,7 +10,7 @@ "data": { "password": "Contrase\u00f1a" }, - "description": "Confirma la contrase\u00f1a de la cuenta de Meater Cloud {username}." + "description": "Confirma la contrase\u00f1a de la cuenta de {username} en Meater Cloud." }, "user": { "data": { diff --git a/homeassistant/components/media_extractor/__init__.py b/homeassistant/components/media_extractor/__init__.py index c6f9c666649..081375e8e08 100644 --- a/homeassistant/components/media_extractor/__init__.py +++ b/homeassistant/components/media_extractor/__init__.py @@ -5,11 +5,11 @@ import voluptuous as vol from youtube_dl import YoutubeDL from youtube_dl.utils import DownloadError, ExtractorError -from homeassistant.components.media_player import MEDIA_PLAYER_PLAY_MEDIA_SCHEMA -from homeassistant.components.media_player.const import ( +from homeassistant.components.media_player import ( ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_TYPE, DOMAIN as MEDIA_PLAYER_DOMAIN, + MEDIA_PLAYER_PLAY_MEDIA_SCHEMA, SERVICE_PLAY_MEDIA, ) from homeassistant.const import ATTR_ENTITY_ID diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 29c75a4fc22..5771e1b6938 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -12,25 +12,23 @@ import hashlib from http import HTTPStatus import logging import secrets -from typing import Any, cast, final -from urllib.parse import urlparse +from typing import Any, Final, TypedDict, final +from urllib.parse import quote, urlparse from aiohttp import web from aiohttp.hdrs import CACHE_CONTROL, CONTENT_TYPE from aiohttp.typedefs import LooseHeaders import async_timeout +from typing_extensions import Required import voluptuous as vol from yarl import URL from homeassistant.backports.enum import StrEnum from homeassistant.components import websocket_api from homeassistant.components.http import KEY_AUTHENTICATED, HomeAssistantView -from homeassistant.components.websocket_api.const import ( - ERR_NOT_SUPPORTED, - ERR_UNKNOWN_ERROR, -) +from homeassistant.components.websocket_api import ERR_NOT_SUPPORTED, ERR_UNKNOWN_ERROR from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( +from homeassistant.const import ( # noqa: F401 SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY, @@ -127,22 +125,23 @@ from .const import ( # noqa: F401 SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, + MediaClass, MediaPlayerEntityFeature, + MediaPlayerState, + MediaType, + RepeatMode, ) from .errors import BrowseError -# mypy: allow-untyped-defs, no-check-untyped-defs - _LOGGER = logging.getLogger(__name__) ENTITY_ID_FORMAT = DOMAIN + ".{}" -CACHE_IMAGES = "images" -CACHE_MAXSIZE = "maxsize" -CACHE_LOCK = "lock" -CACHE_URL = "url" -CACHE_CONTENT = "content" -ENTITY_IMAGE_CACHE = {CACHE_IMAGES: collections.OrderedDict(), CACHE_MAXSIZE: 16} +CACHE_IMAGES: Final = "images" +CACHE_MAXSIZE: Final = "maxsize" +CACHE_LOCK: Final = "lock" +CACHE_URL: Final = "url" +CACHE_CONTENT: Final = "content" SCAN_INTERVAL = dt.timedelta(seconds=10) @@ -215,9 +214,28 @@ ATTR_TO_PROPERTY = [ ATTR_MEDIA_REPEAT, ] +# mypy: disallow-any-generics + + +class _CacheImage(TypedDict, total=False): + """Class to hold a cached image.""" + + lock: Required[asyncio.Lock] + content: tuple[bytes | None, str | None] + + +class _ImageCache(TypedDict): + """Class to hold a cached image.""" + + images: collections.OrderedDict[str, _CacheImage] + maxsize: int + + +_ENTITY_IMAGE_CACHE = _ImageCache(images=collections.OrderedDict(), maxsize=16) + @bind_hass -def is_on(hass, entity_id=None): +def is_on(hass: HomeAssistant, entity_id: str | None = None) -> bool: """ Return true if specified media player entity_id is on. @@ -225,7 +243,8 @@ def is_on(hass, entity_id=None): """ entity_ids = [entity_id] if entity_id else hass.states.entity_ids(DOMAIN) return any( - not hass.states.is_state(entity_id, STATE_OFF) for entity_id in entity_ids + not hass.states.is_state(entity_id, MediaPlayerState.OFF) + for entity_id in entity_ids ) @@ -248,7 +267,7 @@ def _rename_keys(**keys: Any) -> Callable[[dict[str, Any]], dict[str, Any]]: async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Track states and offer events for media_players.""" - component = hass.data[DOMAIN] = EntityComponent( + component = hass.data[DOMAIN] = EntityComponent[MediaPlayerEntity]( logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL ) @@ -367,7 +386,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: ) # Remove in Home Assistant 2022.9 - def _rewrite_enqueue(value): + def _rewrite_enqueue(value: dict[str, Any]) -> dict[str, Any]: """Rewrite the enqueue value.""" if ATTR_MEDIA_ENQUEUE not in value: pass @@ -410,7 +429,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: component.async_register_entity_service( SERVICE_REPEAT_SET, - {vol.Required(ATTR_MEDIA_REPEAT): vol.In(REPEAT_MODES)}, + {vol.Required(ATTR_MEDIA_REPEAT): vol.Coerce(RepeatMode)}, "async_set_repeat", [MediaPlayerEntityFeature.REPEAT_SET], ) @@ -420,13 +439,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry.""" - component: EntityComponent = hass.data[DOMAIN] + component: EntityComponent[MediaPlayerEntity] = hass.data[DOMAIN] return await component.async_setup_entry(entry) async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - component: EntityComponent = hass.data[DOMAIN] + component: EntityComponent[MediaPlayerEntity] = hass.data[DOMAIN] return await component.async_unload_entry(entry) @@ -453,7 +472,7 @@ class MediaPlayerEntity(Entity): _attr_media_artist: str | None = None _attr_media_channel: str | None = None _attr_media_content_id: str | None = None - _attr_media_content_type: str | None = None + _attr_media_content_type: MediaType | str | None = None _attr_media_duration: int | None = None _attr_media_episode: str | None = None _attr_media_image_hash: str | None @@ -466,13 +485,13 @@ class MediaPlayerEntity(Entity): _attr_media_series_title: str | None = None _attr_media_title: str | None = None _attr_media_track: int | None = None - _attr_repeat: str | None = None + _attr_repeat: RepeatMode | str | None = None _attr_shuffle: bool | None = None _attr_sound_mode_list: list[str] | None = None _attr_sound_mode: str | None = None _attr_source_list: list[str] | None = None _attr_source: str | None = None - _attr_state: str | None = None + _attr_state: MediaPlayerState | str | None = None _attr_supported_features: int = 0 _attr_volume_level: float | None = None @@ -487,7 +506,7 @@ class MediaPlayerEntity(Entity): return None @property - def state(self) -> str | None: + def state(self) -> MediaPlayerState | str | None: """State of the player.""" return self._attr_state @@ -514,7 +533,7 @@ class MediaPlayerEntity(Entity): return self._attr_media_content_id @property - def media_content_type(self) -> str | None: + def media_content_type(self) -> MediaType | str | None: """Content type of current playing media.""" return self._attr_media_content_type @@ -663,7 +682,7 @@ class MediaPlayerEntity(Entity): return self._attr_shuffle @property - def repeat(self) -> str | None: + def repeat(self) -> RepeatMode | str | None: """Return current repeat mode.""" return self._attr_repeat @@ -757,12 +776,14 @@ class MediaPlayerEntity(Entity): """Send seek command.""" await self.hass.async_add_executor_job(self.media_seek, position) - def play_media(self, media_type: str, media_id: str, **kwargs: Any) -> None: + def play_media( + self, media_type: MediaType | str, media_id: str, **kwargs: Any + ) -> None: """Play a piece of media.""" raise NotImplementedError() 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.""" await self.hass.async_add_executor_job( @@ -801,11 +822,11 @@ class MediaPlayerEntity(Entity): """Enable/disable shuffle mode.""" await self.hass.async_add_executor_job(self.set_shuffle, shuffle) - def set_repeat(self, repeat: str) -> None: + def set_repeat(self, repeat: RepeatMode) -> None: """Set repeat mode.""" raise NotImplementedError() - async def async_set_repeat(self, repeat: str) -> None: + async def async_set_repeat(self, repeat: RepeatMode) -> None: """Set repeat mode.""" await self.hass.async_add_executor_job(self.set_repeat, repeat) @@ -904,7 +925,11 @@ class MediaPlayerEntity(Entity): ) return - if self.state in (STATE_OFF, STATE_IDLE, STATE_STANDBY): + if self.state in { + MediaPlayerState.OFF, + MediaPlayerState.IDLE, + MediaPlayerState.STANDBY, + }: await self.async_turn_on() else: await self.async_turn_off() @@ -953,7 +978,7 @@ class MediaPlayerEntity(Entity): ) return - if self.state == STATE_PLAYING: + if self.state == MediaPlayerState.PLAYING: await self.async_media_pause() else: await self.async_media_play() @@ -961,7 +986,7 @@ class MediaPlayerEntity(Entity): @property def entity_picture(self) -> str | None: """Return image of the media playing.""" - if self.state == STATE_OFF: + if self.state == MediaPlayerState.OFF: return None if self.media_image_remotely_accessible: @@ -1007,7 +1032,7 @@ class MediaPlayerEntity(Entity): if self.support_grouping: state_attr[ATTR_GROUP_MEMBERS] = self.group_members - if self.state == STATE_OFF: + if self.state == MediaPlayerState.OFF: return state_attr for attr in ATTR_TO_PROPERTY: @@ -1054,8 +1079,8 @@ class MediaPlayerEntity(Entity): Images are cached in memory (the images are typically 10-100kB in size). """ - cache_images = cast(collections.OrderedDict, ENTITY_IMAGE_CACHE[CACHE_IMAGES]) - cache_maxsize = cast(int, ENTITY_IMAGE_CACHE[CACHE_MAXSIZE]) + cache_images = _ENTITY_IMAGE_CACHE[CACHE_IMAGES] + cache_maxsize = _ENTITY_IMAGE_CACHE[CACHE_MAXSIZE] if urlparse(url).hostname is None: url = f"{get_url(self.hass)}{url}" @@ -1065,7 +1090,7 @@ class MediaPlayerEntity(Entity): async with cache_images[url][CACHE_LOCK]: if CACHE_CONTENT in cache_images[url]: - return cache_images[url][CACHE_CONTENT] # type:ignore[no-any-return] + return cache_images[url][CACHE_CONTENT] (content, content_type) = await self._async_fetch_image(url) @@ -1089,7 +1114,9 @@ class MediaPlayerEntity(Entity): """Generate an url for a media browser image.""" url_path = ( f"/api/media_player_proxy/{self.entity_id}/browse_media" - f"/{media_content_type}/{media_content_id}" + # quote the media_content_id as it may contain url unsafe characters + # aiohttp will unquote the path automatically + f"/{media_content_type}/{quote(media_content_id)}" ) url_query = {"token": self.access_token} @@ -1106,10 +1133,13 @@ class MediaPlayerImageView(HomeAssistantView): url = "/api/media_player_proxy/{entity_id}" name = "api:media_player:image" extra_urls = [ - url + "/browse_media/{media_content_type}/{media_content_id}", + # Need to modify the default regex for media_content_id as it may + # include arbitrary characters including '/','{', or '}' + url + + "/browse_media/{media_content_type}/{media_content_id:.+}", ] - def __init__(self, component: EntityComponent) -> None: + def __init__(self, component: EntityComponent[MediaPlayerEntity]) -> None: """Initialize a media player view.""" self.component = component @@ -1170,14 +1200,18 @@ class MediaPlayerImageView(HomeAssistantView): } ) @websocket_api.async_response -async def websocket_browse_media(hass, connection, msg): +async def websocket_browse_media( + hass: HomeAssistant, + connection: websocket_api.connection.ActiveConnection, + msg: dict[str, Any], +) -> None: """ Browse media available to the media_player entity. To use, media_player integrations can implement MediaPlayerEntity.async_browse_media() """ - component = hass.data[DOMAIN] - player: MediaPlayerEntity | None = component.get_entity(msg["entity_id"]) + component: EntityComponent[MediaPlayerEntity] = hass.data[DOMAIN] + player = component.get_entity(msg["entity_id"]) if player is None: connection.send_error(msg["id"], "entity_not_found", "Entity not found") @@ -1197,6 +1231,7 @@ async def websocket_browse_media(hass, connection, msg): try: payload = await player.async_browse_media(media_content_type, media_content_id) except NotImplementedError: + assert player.platform _LOGGER.error( "%s allows media browsing but its integration (%s) does not", player.entity_id, @@ -1218,11 +1253,12 @@ async def websocket_browse_media(hass, connection, msg): # For backwards compat if isinstance(payload, BrowseMedia): - payload = payload.as_dict() + result = payload.as_dict() else: + result = payload # type: ignore[unreachable] _LOGGER.warning("Browse Media should use new BrowseMedia class") - connection.send_result(msg["id"], payload) + connection.send_result(msg["id"], result) async def async_fetch_image( diff --git a/homeassistant/components/media_player/browse_media.py b/homeassistant/components/media_player/browse_media.py index e3474eeb58e..81ded203e75 100644 --- a/homeassistant/components/media_player/browse_media.py +++ b/homeassistant/components/media_player/browse_media.py @@ -19,7 +19,7 @@ from homeassistant.helpers.network import ( is_hass_url, ) -from .const import CONTENT_AUTH_EXPIRY_TIME, MEDIA_CLASS_DIRECTORY +from .const import CONTENT_AUTH_EXPIRY_TIME, MediaClass, MediaType # Paths that we don't need to sign PATHS_WITHOUT_AUTH = ("/api/tts_proxy/",) @@ -92,14 +92,14 @@ class BrowseMedia: def __init__( self, *, - media_class: str, + media_class: MediaClass | str, media_content_id: str, - media_content_type: str, + media_content_type: MediaType | str, title: str, can_play: bool, can_expand: bool, children: Sequence[BrowseMedia] | None = None, - children_media_class: str | None = None, + children_media_class: MediaClass | str | None = None, thumbnail: str | None = None, not_shown: int = 0, ) -> None: @@ -115,7 +115,7 @@ class BrowseMedia: self.thumbnail = thumbnail self.not_shown = not_shown - def as_dict(self, *, parent: bool = True) -> dict: + def as_dict(self, *, parent: bool = True) -> dict[str, Any]: """Convert Media class to browse media dictionary.""" if self.children_media_class is None and self.children: self.calculate_children_class() @@ -147,7 +147,7 @@ class BrowseMedia: def calculate_children_class(self) -> None: """Count the children media classes and calculate the correct class.""" - self.children_media_class = MEDIA_CLASS_DIRECTORY + self.children_media_class = MediaClass.DIRECTORY assert self.children is not None proposed_class = self.children[0].media_class if all(child.media_class == proposed_class for child in self.children): diff --git a/homeassistant/components/media_player/const.py b/homeassistant/components/media_player/const.py index 4d534467ad6..ea8069cc7e4 100644 --- a/homeassistant/components/media_player/const.py +++ b/homeassistant/components/media_player/const.py @@ -1,6 +1,8 @@ """Provides the constants needed for component.""" from enum import IntEnum +from homeassistant.backports.enum import StrEnum + # How long our auth signature on the content should be valid for CONTENT_AUTH_EXPIRY_TIME = 3600 * 24 @@ -38,6 +40,46 @@ ATTR_SOUND_MODE_LIST = "sound_mode_list" DOMAIN = "media_player" + +class MediaPlayerState(StrEnum): + """State of media player entities.""" + + OFF = "off" + ON = "on" + IDLE = "idle" + PLAYING = "playing" + PAUSED = "paused" + STANDBY = "standby" + BUFFERING = "buffering" + + +class MediaClass(StrEnum): + """Media class for media player entities.""" + + ALBUM = "album" + APP = "app" + ARTIST = "artist" + CHANNEL = "channel" + COMPOSER = "composer" + CONTRIBUTING_ARTIST = "contributing_artist" + DIRECTORY = "directory" + EPISODE = "episode" + GAME = "game" + GENRE = "genre" + IMAGE = "image" + MOVIE = "movie" + MUSIC = "music" + PLAYLIST = "playlist" + PODCAST = "podcast" + SEASON = "season" + TRACK = "track" + TV_SHOW = "tv_show" + URL = "url" + VIDEO = "video" + + +# These MEDIA_CLASS_* constants are deprecated as of Home Assistant 2022.10. +# Please use the MediaClass enum instead. MEDIA_CLASS_ALBUM = "album" MEDIA_CLASS_APP = "app" MEDIA_CLASS_ARTIST = "artist" @@ -59,6 +101,35 @@ MEDIA_CLASS_TV_SHOW = "tv_show" MEDIA_CLASS_URL = "url" MEDIA_CLASS_VIDEO = "video" + +class MediaType(StrEnum): + """Media type for media player entities.""" + + ALBUM = "album" + APP = "app" + APPS = "apps" + ARTIST = "artist" + CHANNEL = "channel" + CHANNELS = "channels" + COMPOSER = "composer" + CONTRIBUTING_ARTIST = "contributing_artist" + EPISODE = "episode" + GAME = "game" + GENRE = "genre" + IMAGE = "image" + MOVIE = "movie" + MUSIC = "music" + PLAYLIST = "playlist" + PODCAST = "podcast" + SEASON = "season" + TRACK = "track" + TVSHOW = "tvshow" + URL = "url" + VIDEO = "video" + + +# These MEDIA_TYPE_* constants are deprecated as of Home Assistant 2022.10. +# Please use the MediaType enum instead. MEDIA_TYPE_ALBUM = "album" MEDIA_TYPE_APP = "app" MEDIA_TYPE_APPS = "apps" @@ -88,6 +159,17 @@ SERVICE_SELECT_SOUND_MODE = "select_sound_mode" SERVICE_SELECT_SOURCE = "select_source" SERVICE_UNJOIN = "unjoin" + +class RepeatMode(StrEnum): + """Repeat mode for media player entities.""" + + ALL = "all" + OFF = "off" + ONE = "one" + + +# These REPEAT_MODE_* constants are deprecated as of Home Assistant 2022.10. +# Please use the RepeatMode enum instead. REPEAT_MODE_ALL = "all" REPEAT_MODE_OFF = "off" REPEAT_MODE_ONE = "one" diff --git a/homeassistant/components/media_player/device_condition.py b/homeassistant/components/media_player/device_condition.py index 2f398790ac3..3bf6c5956fa 100644 --- a/homeassistant/components/media_player/device_condition.py +++ b/homeassistant/components/media_player/device_condition.py @@ -46,7 +46,7 @@ async def async_get_conditions( ) -> list[dict[str, str]]: """List device conditions for Media player devices.""" registry = entity_registry.async_get(hass) - conditions = [] + conditions: list[dict[str, str]] = [] # Get all the integrations entities for this device for entry in entity_registry.async_entries_for_device(registry, device_id): diff --git a/homeassistant/components/media_player/manifest.json b/homeassistant/components/media_player/manifest.json index 118d05036cc..4b8b9013b98 100644 --- a/homeassistant/components/media_player/manifest.json +++ b/homeassistant/components/media_player/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/media_player", "dependencies": ["http"], "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "entity" } diff --git a/homeassistant/components/media_player/reproduce_state.py b/homeassistant/components/media_player/reproduce_state.py index bdfc0bf3acb..387792a1b60 100644 --- a/homeassistant/components/media_player/reproduce_state.py +++ b/homeassistant/components/media_player/reproduce_state.py @@ -37,8 +37,6 @@ from .const import ( MediaPlayerEntityFeature, ) -# mypy: allow-untyped-defs - async def _async_reproduce_states( hass: HomeAssistant, @@ -51,7 +49,7 @@ async def _async_reproduce_states( cur_state = hass.states.get(state.entity_id) features = cur_state.attributes[ATTR_SUPPORTED_FEATURES] if cur_state else 0 - async def call_service(service: str, keys: Iterable) -> None: + async def call_service(service: str, keys: Iterable[str]) -> None: """Call service with set of attributes given.""" data = {"entity_id": state.entity_id} for key in keys: diff --git a/homeassistant/components/media_source/__init__.py b/homeassistant/components/media_source/__init__.py index 4818934d1dd..47a5d7f6969 100644 --- a/homeassistant/components/media_source/__init__.py +++ b/homeassistant/components/media_source/__init__.py @@ -26,9 +26,15 @@ from homeassistant.helpers.typing import UNDEFINED, ConfigType, UndefinedType from homeassistant.loader import bind_hass from . import local_source -from .const import DOMAIN, URI_SCHEME, URI_SCHEME_REGEX +from .const import ( + DOMAIN, + MEDIA_CLASS_MAP, + MEDIA_MIME_TYPES, + URI_SCHEME, + URI_SCHEME_REGEX, +) from .error import MediaSourceError, Unresolvable -from .models import BrowseMediaSource, MediaSourceItem, PlayMedia +from .models import BrowseMediaSource, MediaSource, MediaSourceItem, PlayMedia __all__ = [ "DOMAIN", @@ -40,7 +46,10 @@ __all__ = [ "PlayMedia", "MediaSourceItem", "Unresolvable", + "MediaSource", "MediaSourceError", + "MEDIA_CLASS_MAP", + "MEDIA_MIME_TYPES", ] diff --git a/homeassistant/components/media_source/const.py b/homeassistant/components/media_source/const.py index d146cd953f8..73599efb6c3 100644 --- a/homeassistant/components/media_source/const.py +++ b/homeassistant/components/media_source/const.py @@ -1,18 +1,14 @@ """Constants for the media_source integration.""" import re -from homeassistant.components.media_player.const import ( - MEDIA_CLASS_IMAGE, - MEDIA_CLASS_MUSIC, - MEDIA_CLASS_VIDEO, -) +from homeassistant.components.media_player import MediaClass DOMAIN = "media_source" MEDIA_MIME_TYPES = ("audio", "video", "image") MEDIA_CLASS_MAP = { - "audio": MEDIA_CLASS_MUSIC, - "video": MEDIA_CLASS_VIDEO, - "image": MEDIA_CLASS_IMAGE, + "audio": MediaClass.MUSIC, + "video": MediaClass.VIDEO, + "image": MediaClass.IMAGE, } URI_SCHEME = "media-source://" URI_SCHEME_REGEX = re.compile( diff --git a/homeassistant/components/media_source/local_source.py b/homeassistant/components/media_source/local_source.py index 863380b7600..1ec3d6f9462 100644 --- a/homeassistant/components/media_source/local_source.py +++ b/homeassistant/components/media_source/local_source.py @@ -11,8 +11,7 @@ from aiohttp.web_request import FileField import voluptuous as vol from homeassistant.components import http, websocket_api -from homeassistant.components.media_player.const import MEDIA_CLASS_DIRECTORY -from homeassistant.components.media_player.errors import BrowseError +from homeassistant.components.media_player import BrowseError, MediaClass from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import Unauthorized from homeassistant.util import raise_if_invalid_filename, raise_if_invalid_path @@ -109,12 +108,12 @@ class LocalSource(MediaSource): base = BrowseMediaSource( domain=DOMAIN, identifier="", - media_class=MEDIA_CLASS_DIRECTORY, + media_class=MediaClass.DIRECTORY, media_content_type=None, title=self.name, can_play=False, can_expand=True, - children_media_class=MEDIA_CLASS_DIRECTORY, + children_media_class=MediaClass.DIRECTORY, ) base.children = [ @@ -158,10 +157,10 @@ class LocalSource(MediaSource): title = path.name - media_class = MEDIA_CLASS_DIRECTORY + media_class = MediaClass.DIRECTORY if mime_type: media_class = MEDIA_CLASS_MAP.get( - mime_type.split("/")[0], MEDIA_CLASS_DIRECTORY + mime_type.split("/")[0], MediaClass.DIRECTORY ) media = BrowseMediaSource( diff --git a/homeassistant/components/media_source/manifest.json b/homeassistant/components/media_source/manifest.json index 3b00df4300b..ae65137c113 100644 --- a/homeassistant/components/media_source/manifest.json +++ b/homeassistant/components/media_source/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/media_source", "dependencies": ["http"], "codeowners": ["@hunterjm"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "system" } diff --git a/homeassistant/components/media_source/models.py b/homeassistant/components/media_source/models.py index f6772bc6ad9..3bf77daf691 100644 --- a/homeassistant/components/media_source/models.py +++ b/homeassistant/components/media_source/models.py @@ -5,12 +5,7 @@ from abc import ABC from dataclasses import dataclass from typing import Any, cast -from homeassistant.components.media_player import BrowseMedia -from homeassistant.components.media_player.const import ( - MEDIA_CLASS_APP, - MEDIA_TYPE_APP, - MEDIA_TYPE_APPS, -) +from homeassistant.components.media_player import BrowseMedia, MediaClass, MediaType from homeassistant.core import HomeAssistant, callback from .const import DOMAIN, URI_SCHEME, URI_SCHEME_REGEX @@ -56,20 +51,20 @@ class MediaSourceItem: base = BrowseMediaSource( domain=None, identifier=None, - media_class=MEDIA_CLASS_APP, - media_content_type=MEDIA_TYPE_APPS, + media_class=MediaClass.APP, + media_content_type=MediaType.APPS, title="Media Sources", can_play=False, can_expand=True, - children_media_class=MEDIA_CLASS_APP, + children_media_class=MediaClass.APP, ) base.children = sorted( ( BrowseMediaSource( domain=source.domain, identifier=None, - media_class=MEDIA_CLASS_APP, - media_content_type=MEDIA_TYPE_APP, + media_class=MediaClass.APP, + media_content_type=MediaType.APP, thumbnail=f"https://brands.home-assistant.io/_/{source.domain}/logo.png", title=source.name, can_play=False, diff --git a/homeassistant/components/mediaroom/media_player.py b/homeassistant/components/mediaroom/media_player.py index b3a800c489e..b5089c64b14 100644 --- a/homeassistant/components/mediaroom/media_player.py +++ b/homeassistant/components/mediaroom/media_player.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any from pymediaroom import ( COMMANDS, @@ -16,19 +17,15 @@ from homeassistant.components.media_player import ( PLATFORM_SCHEMA, MediaPlayerEntity, MediaPlayerEntityFeature, + MediaPlayerState, + MediaType, ) -from homeassistant.components.media_player.const import MEDIA_TYPE_CHANNEL from homeassistant.const import ( CONF_HOST, CONF_NAME, CONF_OPTIMISTIC, CONF_TIMEOUT, EVENT_HOMEASSISTANT_STOP, - STATE_OFF, - STATE_PAUSED, - STATE_PLAYING, - STATE_STANDBY, - STATE_UNAVAILABLE, ) from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv @@ -117,6 +114,7 @@ async def async_setup_platform( class MediaroomDevice(MediaPlayerEntity): """Representation of a Mediaroom set-up-box on the network.""" + _attr_media_content_type = MediaType.CHANNEL _attr_should_poll = False _attr_supported_features = ( MediaPlayerEntityFeature.PAUSE @@ -135,13 +133,13 @@ class MediaroomDevice(MediaPlayerEntity): """Map pymediaroom state to HA state.""" state_map = { - State.OFF: STATE_OFF, - State.STANDBY: STATE_STANDBY, - State.PLAYING_LIVE_TV: STATE_PLAYING, - State.PLAYING_RECORDED_TV: STATE_PLAYING, - State.PLAYING_TIMESHIFT_TV: STATE_PLAYING, - State.STOPPED: STATE_PAUSED, - State.UNKNOWN: STATE_UNAVAILABLE, + State.OFF: MediaPlayerState.OFF, + State.STANDBY: MediaPlayerState.STANDBY, + State.PLAYING_LIVE_TV: MediaPlayerState.PLAYING, + State.PLAYING_RECORDED_TV: MediaPlayerState.PLAYING, + State.PLAYING_TIMESHIFT_TV: MediaPlayerState.PLAYING, + State.STOPPED: MediaPlayerState.PAUSED, + State.UNKNOWN: None, } self._state = state_map[mediaroom_state] @@ -156,7 +154,9 @@ class MediaroomDevice(MediaPlayerEntity): ) self._channel = None self._optimistic = optimistic - self._state = STATE_PLAYING if optimistic else STATE_STANDBY + self._state = ( + MediaPlayerState.PLAYING if optimistic else MediaPlayerState.STANDBY + ) self._name = f"Mediaroom {device_id if device_id else host}" self._available = True if device_id: @@ -169,7 +169,7 @@ class MediaroomDevice(MediaPlayerEntity): """Return True if entity is available.""" return self._available - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Retrieve latest state.""" async def async_notify_received(notify): @@ -189,29 +189,33 @@ class MediaroomDevice(MediaPlayerEntity): ) ) - async def async_play_media(self, media_type, media_id, **kwargs): + async def async_play_media( + self, media_type: MediaType | str, media_id: str, **kwargs: Any + ) -> None: """Play media.""" _LOGGER.debug( "STB(%s) Play media: %s (%s)", self.stb.stb_ip, media_id, media_type ) - if media_type == MEDIA_TYPE_CHANNEL: + command: str | int + if media_type == MediaType.CHANNEL: if not media_id.isdigit(): _LOGGER.error("Invalid media_id %s: Must be a channel number", media_id) return - media_id = int(media_id) + command = int(media_id) elif media_type == MEDIA_TYPE_MEDIAROOM: if media_id not in COMMANDS: _LOGGER.error("Invalid media_id %s: Must be a command", media_id) return + command = media_id else: _LOGGER.error("Invalid media type %s", media_type) return try: - await self.stb.send_cmd(media_id) + await self.stb.send_cmd(command) if self._optimistic: - self._state = STATE_PLAYING + self._state = MediaPlayerState.PLAYING self._available = True except PyMediaroomError: self._available = False @@ -232,102 +236,97 @@ class MediaroomDevice(MediaPlayerEntity): """Return the state of the device.""" return self._state - @property - def media_content_type(self): - """Return the content type of current playing media.""" - return MEDIA_TYPE_CHANNEL - @property def media_channel(self): """Channel currently playing.""" return self._channel - async def async_turn_on(self): + async def async_turn_on(self) -> None: """Turn on the receiver.""" try: self.set_state(await self.stb.turn_on()) if self._optimistic: - self._state = STATE_PLAYING + self._state = MediaPlayerState.PLAYING self._available = True except PyMediaroomError: self._available = False self.async_write_ha_state() - async def async_turn_off(self): + async def async_turn_off(self) -> None: """Turn off the receiver.""" try: self.set_state(await self.stb.turn_off()) if self._optimistic: - self._state = STATE_STANDBY + self._state = MediaPlayerState.STANDBY self._available = True except PyMediaroomError: self._available = False self.async_write_ha_state() - async def async_media_play(self): + async def async_media_play(self) -> None: """Send play command.""" try: _LOGGER.debug("media_play()") await self.stb.send_cmd("PlayPause") if self._optimistic: - self._state = STATE_PLAYING + self._state = MediaPlayerState.PLAYING self._available = True except PyMediaroomError: self._available = False self.async_write_ha_state() - async def async_media_pause(self): + async def async_media_pause(self) -> None: """Send pause command.""" try: await self.stb.send_cmd("PlayPause") if self._optimistic: - self._state = STATE_PAUSED + self._state = MediaPlayerState.PAUSED self._available = True except PyMediaroomError: self._available = False self.async_write_ha_state() - async def async_media_stop(self): + async def async_media_stop(self) -> None: """Send stop command.""" try: await self.stb.send_cmd("Stop") if self._optimistic: - self._state = STATE_PAUSED + self._state = MediaPlayerState.PAUSED self._available = True except PyMediaroomError: self._available = False self.async_write_ha_state() - async def async_media_previous_track(self): + async def async_media_previous_track(self) -> None: """Send Program Down command.""" try: await self.stb.send_cmd("ProgDown") if self._optimistic: - self._state = STATE_PLAYING + self._state = MediaPlayerState.PLAYING self._available = True except PyMediaroomError: self._available = False self.async_write_ha_state() - async def async_media_next_track(self): + async def async_media_next_track(self) -> None: """Send Program Up command.""" try: await self.stb.send_cmd("ProgUp") if self._optimistic: - self._state = STATE_PLAYING + self._state = MediaPlayerState.PLAYING self._available = True except PyMediaroomError: self._available = False self.async_write_ha_state() - async def async_volume_up(self): + async def async_volume_up(self) -> None: """Send volume up command.""" try: @@ -337,7 +336,7 @@ class MediaroomDevice(MediaPlayerEntity): self._available = False self.async_write_ha_state() - async def async_volume_down(self): + async def async_volume_down(self) -> None: """Send volume up command.""" try: @@ -346,7 +345,7 @@ class MediaroomDevice(MediaPlayerEntity): self._available = False self.async_write_ha_state() - async def async_mute_volume(self, mute): + async def async_mute_volume(self, mute: bool) -> None: """Send mute command.""" try: diff --git a/homeassistant/components/melcloud/__init__.py b/homeassistant/components/melcloud/__init__.py index d8a044d23f6..5eefb3db487 100644 --- a/homeassistant/components/melcloud/__init__.py +++ b/homeassistant/components/melcloud/__init__.py @@ -141,6 +141,11 @@ class MelCloudDevice: name=self.name, ) + @property + def daily_energy_consumed(self) -> float | None: + """Return energy consumed during the current day in kWh.""" + return self.device.daily_energy_consumed + async def mel_devices_setup(hass, token) -> dict[str, list[MelCloudDevice]]: """Query connected devices from MELCloud.""" diff --git a/homeassistant/components/melcloud/climate.py b/homeassistant/components/melcloud/climate.py index a0ffe3a68bb..9fc455893f6 100644 --- a/homeassistant/components/melcloud/climate.py +++ b/homeassistant/components/melcloud/climate.py @@ -14,11 +14,11 @@ from pymelcloud.atw_device import ( ) import voluptuous as vol -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( ATTR_HVAC_MODE, DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP, + ClimateEntity, ClimateEntityFeature, HVACMode, ) @@ -105,7 +105,7 @@ class MelCloudClimate(ClimateEntity): self.api = device self._base_device = self.api.device - async def async_update(self): + async def async_update(self) -> None: """Update state from MELCloud.""" await self.api.async_update() @@ -257,7 +257,7 @@ class AtaDeviceClimate(MelCloudClimate): """Return vertical vane position or mode.""" return self._device.vane_vertical - async def async_set_swing_mode(self, swing_mode) -> None: + async def async_set_swing_mode(self, swing_mode: str) -> None: """Set vertical vane position or mode.""" await self.async_set_vane_vertical(swing_mode) @@ -362,7 +362,7 @@ class AtwDeviceZoneClimate(MelCloudClimate): """Return the temperature we try to reach.""" return self._zone.target_temperature - async def async_set_temperature(self, **kwargs) -> None: + async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" await self._zone.set_target_temperature( kwargs.get("temperature", self.target_temperature) diff --git a/homeassistant/components/melcloud/manifest.json b/homeassistant/components/melcloud/manifest.json index 2f209667daf..f4853f48c8c 100644 --- a/homeassistant/components/melcloud/manifest.json +++ b/homeassistant/components/melcloud/manifest.json @@ -3,7 +3,7 @@ "name": "MELCloud", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/melcloud", - "requirements": ["pymelcloud==2.5.6"], + "requirements": ["pymelcloud==2.5.8"], "codeowners": ["@vilppuvuorinen"], "iot_class": "cloud_polling", "loggers": ["pymelcloud"] diff --git a/homeassistant/components/melcloud/sensor.py b/homeassistant/components/melcloud/sensor.py index ed0eac98989..3d407b0916b 100644 --- a/homeassistant/components/melcloud/sensor.py +++ b/homeassistant/components/melcloud/sensor.py @@ -57,6 +57,15 @@ ATA_SENSORS: tuple[MelcloudSensorEntityDescription, ...] = ( value_fn=lambda x: x.device.total_energy_consumed, enabled=lambda x: x.device.has_energy_consumed_meter, ), + MelcloudSensorEntityDescription( + key="daily_energy", + name="Daily Energy Consumed", + icon="mdi:factory", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + value_fn=lambda x: x.device.daily_energy_consumed, + enabled=lambda x: True, + ), ) ATW_SENSORS: tuple[MelcloudSensorEntityDescription, ...] = ( MelcloudSensorEntityDescription( @@ -77,6 +86,15 @@ ATW_SENSORS: tuple[MelcloudSensorEntityDescription, ...] = ( value_fn=lambda x: x.device.tank_temperature, enabled=lambda x: True, ), + MelcloudSensorEntityDescription( + key="daily_energy", + name="Daily Energy Consumed", + icon="mdi:factory", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + value_fn=lambda x: x.device.daily_energy_consumed, + enabled=lambda x: True, + ), ) ATW_ZONE_SENSORS: tuple[MelcloudSensorEntityDescription, ...] = ( MelcloudSensorEntityDescription( @@ -165,7 +183,7 @@ class MelDeviceSensor(SensorEntity): """Return the state of the sensor.""" return self.entity_description.value_fn(self._api) - async def async_update(self): + async def async_update(self) -> None: """Retrieve latest state.""" await self._api.async_update() diff --git a/homeassistant/components/melcloud/water_heater.py b/homeassistant/components/melcloud/water_heater.py index 58da39b1a61..49c642d562d 100644 --- a/homeassistant/components/melcloud/water_heater.py +++ b/homeassistant/components/melcloud/water_heater.py @@ -1,6 +1,8 @@ """Platform for water_heater integration.""" from __future__ import annotations +from typing import Any + from pymelcloud import DEVICE_TYPE_ATW, AtwDevice from pymelcloud.atw_device import ( PROPERTY_OPERATION_MODE, @@ -51,7 +53,7 @@ class AtwWaterHeater(WaterHeaterEntity): self._device = device self._name = device.name - async def async_update(self): + async def async_update(self) -> None: """Update state from MELCloud.""" await self._api.async_update() @@ -109,7 +111,7 @@ class AtwWaterHeater(WaterHeaterEntity): """Return the temperature we try to reach.""" return self._device.target_tank_temperature - async def async_set_temperature(self, **kwargs): + async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" await self._device.set( { @@ -119,7 +121,7 @@ class AtwWaterHeater(WaterHeaterEntity): } ) - async def async_set_operation_mode(self, operation_mode): + async def async_set_operation_mode(self, operation_mode: str) -> None: """Set new target operation mode.""" await self._device.set({PROPERTY_OPERATION_MODE: operation_mode}) diff --git a/homeassistant/components/melissa/climate.py b/homeassistant/components/melissa/climate.py index 7dae7c2dad6..bfe1a4929d0 100644 --- a/homeassistant/components/melissa/climate.py +++ b/homeassistant/components/melissa/climate.py @@ -2,13 +2,14 @@ from __future__ import annotations import logging +from typing import Any -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( FAN_AUTO, FAN_HIGH, FAN_LOW, FAN_MEDIUM, + ClimateEntity, ClimateEntityFeature, HVACMode, ) @@ -135,12 +136,12 @@ class MelissaClimate(ClimateEntity): """Return the maximum supported temperature for the thermostat.""" return 30 - async def async_set_temperature(self, **kwargs): + async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" temp = kwargs.get(ATTR_TEMPERATURE) await self.async_send({self._api.TEMP: temp}) - async def async_set_fan_mode(self, fan_mode): + async def async_set_fan_mode(self, fan_mode: str) -> None: """Set fan mode.""" melissa_fan_mode = self.hass_fan_to_melissa(fan_mode) await self.async_send({self._api.FAN: melissa_fan_mode}) @@ -168,7 +169,7 @@ class MelissaClimate(ClimateEntity): ): self._cur_settings = old_value - async def async_update(self): + async def async_update(self) -> None: """Get latest data from Melissa.""" try: self._data = (await self._api.async_status(cached=True))[ diff --git a/homeassistant/components/melnor/__init__.py b/homeassistant/components/melnor/__init__.py index 5fd697b2088..93b8d11ab24 100644 --- a/homeassistant/components/melnor/__init__.py +++ b/homeassistant/components/melnor/__init__.py @@ -14,7 +14,11 @@ from homeassistant.exceptions import ConfigEntryNotReady from .const import DOMAIN from .models import MelnorDataUpdateCoordinator -PLATFORMS: list[Platform] = [Platform.SWITCH] +PLATFORMS: list[Platform] = [ + Platform.NUMBER, + Platform.SENSOR, + Platform.SWITCH, +] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/melnor/manifest.json b/homeassistant/components/melnor/manifest.json index a59758f705b..c57549aa647 100644 --- a/homeassistant/components/melnor/manifest.json +++ b/homeassistant/components/melnor/manifest.json @@ -12,5 +12,5 @@ "documentation": "https://www.home-assistant.io/integrations/melnor", "iot_class": "local_polling", "name": "Melnor Bluetooth", - "requirements": ["melnor-bluetooth==0.0.15"] + "requirements": ["melnor-bluetooth==0.0.20"] } diff --git a/homeassistant/components/melnor/models.py b/homeassistant/components/melnor/models.py index 4796bf601ff..8cbe5f80680 100644 --- a/homeassistant/components/melnor/models.py +++ b/homeassistant/components/melnor/models.py @@ -1,10 +1,13 @@ """Melnor integration models.""" +from collections.abc import Callable from datetime import timedelta import logging +from typing import TypeVar -from melnor_bluetooth.device import Device +from melnor_bluetooth.device import Device, Valve +from homeassistant.components.number import EntityDescription from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import ( @@ -39,10 +42,11 @@ class MelnorDataUpdateCoordinator(DataUpdateCoordinator[Device]): return self._device -class MelnorBluetoothBaseEntity(CoordinatorEntity[MelnorDataUpdateCoordinator]): +class MelnorBluetoothEntity(CoordinatorEntity[MelnorDataUpdateCoordinator]): """Base class for melnor entities.""" _device: Device + _attr_has_entity_name = True def __init__( self, @@ -59,8 +63,6 @@ class MelnorBluetoothBaseEntity(CoordinatorEntity[MelnorDataUpdateCoordinator]): model=self._device.model, name=self._device.name, ) - self._attr_name = self._device.name - self._attr_unique_id = self._device.mac @callback def _handle_coordinator_update(self) -> None: @@ -72,3 +74,57 @@ class MelnorBluetoothBaseEntity(CoordinatorEntity[MelnorDataUpdateCoordinator]): def available(self) -> bool: """Return True if entity is available.""" return self._device.is_connected + + +class MelnorZoneEntity(MelnorBluetoothEntity): + """Base class for valves that define themselves as child devices.""" + + _valve: Valve + + def __init__( + self, + coordinator: MelnorDataUpdateCoordinator, + entity_description: EntityDescription, + valve: Valve, + ) -> None: + """Initialize a valve entity.""" + super().__init__(coordinator) + + self._attr_unique_id = ( + f"{self._device.mac}-zone{valve.id}-{entity_description.key}" + ) + self.entity_description = entity_description + + self._valve = valve + + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, f"{self._device.mac}-zone{self._valve.id}")}, + manufacturer="Melnor", + name=f"Zone {valve.id + 1}", + via_device=(DOMAIN, self._device.mac), + ) + + +T = TypeVar("T", bound=EntityDescription) + + +def get_entities_for_valves( + coordinator: MelnorDataUpdateCoordinator, + descriptions: list[T], + function: Callable[ + [Valve, T], + CoordinatorEntity[MelnorDataUpdateCoordinator], + ], +) -> list[CoordinatorEntity[MelnorDataUpdateCoordinator]]: + """Get descriptions for valves.""" + entities = [] + + # This device may not have 4 valves total, but the library will only expose the right number of valves + for i in range(1, 5): + valve = coordinator.data[f"zone{i}"] + + if valve is not None: + for description in descriptions: + entities.append(function(valve, description)) + + return entities diff --git a/homeassistant/components/melnor/number.py b/homeassistant/components/melnor/number.py new file mode 100644 index 00000000000..3f29e7cf772 --- /dev/null +++ b/homeassistant/components/melnor/number.py @@ -0,0 +1,96 @@ +"""Number support for Melnor Bluetooth water timer.""" + +from __future__ import annotations + +from collections.abc import Callable, Coroutine +from dataclasses import dataclass +from typing import Any + +from melnor_bluetooth.device import Valve + +from homeassistant.components.number import NumberEntity, NumberEntityDescription +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 .models import ( + MelnorDataUpdateCoordinator, + MelnorZoneEntity, + get_entities_for_valves, +) + + +@dataclass +class MelnorZoneNumberEntityDescriptionMixin: + """Mixin for required keys.""" + + set_num_fn: Callable[[Valve, int], Coroutine[Any, Any, None]] + state_fn: Callable[[Valve], Any] + + +@dataclass +class MelnorZoneNumberEntityDescription( + NumberEntityDescription, MelnorZoneNumberEntityDescriptionMixin +): + """Describes Melnor number entity.""" + + +ZONE_ENTITY_DESCRIPTIONS: list[MelnorZoneNumberEntityDescription] = [ + MelnorZoneNumberEntityDescription( + entity_category=EntityCategory.CONFIG, + native_max_value=360, + native_min_value=1, + icon="mdi:timer-cog-outline", + key="manual_minutes", + name="Manual Minutes", + set_num_fn=lambda valve, value: valve.set_manual_watering_minutes(value), + state_fn=lambda valve: valve.manual_watering_minutes, + ) +] + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the number platform.""" + + coordinator: MelnorDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + + async_add_entities( + get_entities_for_valves( + coordinator, + ZONE_ENTITY_DESCRIPTIONS, + lambda valve, description: MelnorZoneNumber( + coordinator, description, valve + ), + ) + ) + + +class MelnorZoneNumber(MelnorZoneEntity, NumberEntity): + """A number implementation for a melnor device.""" + + entity_description: MelnorZoneNumberEntityDescription + + def __init__( + self, + coordinator: MelnorDataUpdateCoordinator, + entity_description: MelnorZoneNumberEntityDescription, + valve: Valve, + ) -> None: + """Initialize a number for a melnor device.""" + super().__init__(coordinator, entity_description, valve) + + @property + def native_value(self) -> float | None: + """Return the current value.""" + return self._valve.manual_watering_minutes + + async def async_set_native_value(self, value: float) -> None: + """Update the current value.""" + await self.entity_description.set_num_fn(self._valve, int(value)) + self._async_write_ha_state() diff --git a/homeassistant/components/melnor/sensor.py b/homeassistant/components/melnor/sensor.py new file mode 100644 index 00000000000..42eb9e60c73 --- /dev/null +++ b/homeassistant/components/melnor/sensor.py @@ -0,0 +1,175 @@ +"""Sensor support for Melnor Bluetooth water timer.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +from datetime import datetime +from typing import Any + +from melnor_bluetooth.device import Device, Valve + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import PERCENTAGE, SIGNAL_STRENGTH_DECIBELS_MILLIWATT +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 import dt as dt_util + +from .const import DOMAIN +from .models import ( + MelnorBluetoothEntity, + MelnorDataUpdateCoordinator, + MelnorZoneEntity, + get_entities_for_valves, +) + + +def watering_seconds_left(valve: Valve) -> datetime | None: + """Calculate the number of minutes left in the current watering cycle.""" + + if valve.is_watering is not True or dt_util.now() > dt_util.utc_from_timestamp( + valve.watering_end_time + ): + return None + + return dt_util.utc_from_timestamp(valve.watering_end_time) + + +@dataclass +class MelnorSensorEntityDescriptionMixin: + """Mixin for required keys.""" + + state_fn: Callable[[Device], Any] + + +@dataclass +class MelnorZoneSensorEntityDescriptionMixin: + """Mixin for required keys.""" + + state_fn: Callable[[Valve], Any] + + +@dataclass +class MelnorZoneSensorEntityDescription( + SensorEntityDescription, MelnorZoneSensorEntityDescriptionMixin +): + """Describes Melnor sensor entity.""" + + +@dataclass +class MelnorSensorEntityDescription( + SensorEntityDescription, MelnorSensorEntityDescriptionMixin +): + """Describes Melnor sensor entity.""" + + +DEVICE_ENTITY_DESCRIPTIONS: list[MelnorSensorEntityDescription] = [ + MelnorSensorEntityDescription( + device_class=SensorDeviceClass.BATTERY, + entity_category=EntityCategory.DIAGNOSTIC, + key="battery", + name="Battery", + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + state_fn=lambda device: device.battery_level, + ), + MelnorSensorEntityDescription( + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + key="rssi", + name="RSSI", + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + state_class=SensorStateClass.MEASUREMENT, + state_fn=lambda device: device.rssi, + ), +] + +ZONE_ENTITY_DESCRIPTIONS: list[MelnorZoneSensorEntityDescription] = [ + MelnorZoneSensorEntityDescription( + device_class=SensorDeviceClass.TIMESTAMP, + key="manual_cycle_end", + name="Manual Cycle End", + state_fn=watering_seconds_left, + ), +] + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the sensor platform.""" + + coordinator: MelnorDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + + # Device-level sensors + async_add_entities( + MelnorSensorEntity( + coordinator, + description, + ) + for description in DEVICE_ENTITY_DESCRIPTIONS + ) + + # Valve/Zone-level sensors + async_add_entities( + get_entities_for_valves( + coordinator, + ZONE_ENTITY_DESCRIPTIONS, + lambda valve, description: MelnorZoneSensorEntity( + coordinator, description, valve + ), + ) + ) + + +class MelnorSensorEntity(MelnorBluetoothEntity, SensorEntity): + """Representation of a Melnor sensor.""" + + entity_description: MelnorSensorEntityDescription + + def __init__( + self, + coordinator: MelnorDataUpdateCoordinator, + entity_description: MelnorSensorEntityDescription, + ) -> None: + """Initialize a sensor for a Melnor device.""" + super().__init__(coordinator) + + self._attr_unique_id = f"{self._device.mac}-{entity_description.key}" + + self.entity_description = entity_description + + @property + def native_value(self) -> StateType: + """Return the sensor value.""" + return self.entity_description.state_fn(self._device) + + +class MelnorZoneSensorEntity(MelnorZoneEntity, SensorEntity): + """Representation of a Melnor sensor.""" + + entity_description: MelnorZoneSensorEntityDescription + + def __init__( + self, + coordinator: MelnorDataUpdateCoordinator, + entity_description: MelnorZoneSensorEntityDescription, + valve: Valve, + ) -> None: + """Initialize a sensor for a Melnor device.""" + super().__init__(coordinator, entity_description, valve) + + @property + def native_value(self) -> StateType: + """Return the sensor value.""" + return self.entity_description.state_fn(self._valve) diff --git a/homeassistant/components/melnor/switch.py b/homeassistant/components/melnor/switch.py index 7a615a8582d..eca6f1a98cf 100644 --- a/homeassistant/components/melnor/switch.py +++ b/homeassistant/components/melnor/switch.py @@ -1,72 +1,101 @@ -"""Support for Melnor RainCloud sprinkler water timer.""" +"""Switch support for Melnor Bluetooth water timer.""" from __future__ import annotations -from typing import Any, cast +from collections.abc import Callable, Coroutine +from dataclasses import dataclass +from typing import Any from melnor_bluetooth.device import Valve -from homeassistant.components.switch import SwitchEntity +from homeassistant.components.switch import ( + SwitchDeviceClass, + SwitchEntity, + SwitchEntityDescription, +) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN -from .models import MelnorBluetoothBaseEntity, MelnorDataUpdateCoordinator +from .models import ( + MelnorDataUpdateCoordinator, + MelnorZoneEntity, + get_entities_for_valves, +) + + +@dataclass +class MelnorSwitchEntityDescriptionMixin: + """Mixin for required keys.""" + + on_off_fn: Callable[[Valve, bool], Coroutine[Any, Any, None]] + state_fn: Callable[[Valve], Any] + + +@dataclass +class MelnorSwitchEntityDescription( + SwitchEntityDescription, MelnorSwitchEntityDescriptionMixin +): + """Describes Melnor switch entity.""" + + +ZONE_ENTITY_DESCRIPTIONS = [ + MelnorSwitchEntityDescription( + device_class=SwitchDeviceClass.SWITCH, + icon="mdi:sprinkler", + key="manual", + on_off_fn=lambda valve, bool: valve.set_is_watering(bool), + state_fn=lambda valve: valve.is_watering, + ) +] async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, - async_add_devices: AddEntitiesCallback, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up the switch platform.""" - switches = [] coordinator: MelnorDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] - # This device may not have 4 valves total, but the library will only expose the right number of valves - for i in range(1, 5): - if coordinator.data[f"zone{i}"] is not None: - switches.append(MelnorSwitch(coordinator, i)) - - async_add_devices(switches, True) + async_add_entities( + get_entities_for_valves( + coordinator, + ZONE_ENTITY_DESCRIPTIONS, + lambda valve, description: MelnorZoneSwitch( + coordinator, description, valve + ), + ) + ) -class MelnorSwitch(MelnorBluetoothBaseEntity, SwitchEntity): +class MelnorZoneSwitch(MelnorZoneEntity, SwitchEntity): """A switch implementation for a melnor device.""" - _valve_index: int - _attr_icon = "mdi:sprinkler" + entity_description: MelnorSwitchEntityDescription def __init__( self, coordinator: MelnorDataUpdateCoordinator, - valve_index: int, + entity_description: MelnorSwitchEntityDescription, + valve: Valve, ) -> None: """Initialize a switch for a melnor device.""" - super().__init__(coordinator) - self._valve_index = valve_index - - self._attr_unique_id = f"{self._attr_unique_id}-zone{self._valve().id}-manual" - self._attr_name = f"{self._device.name} Zone {self._valve().id+1}" + super().__init__(coordinator, entity_description, valve) @property def is_on(self) -> bool: """Return true if device is on.""" - return self._valve().is_watering + return self.entity_description.state_fn(self._valve) async def async_turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" - self._valve().is_watering = True - await self._device.push_state() + await self.entity_description.on_off_fn(self._valve, True) self.async_write_ha_state() async def async_turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" - self._valve().is_watering = False - await self._device.push_state() + await self.entity_description.on_off_fn(self._valve, False) self.async_write_ha_state() - - def _valve(self) -> Valve: - return cast(Valve, self._device[f"zone{self._valve_index}"]) diff --git a/homeassistant/components/melnor/translations/ca.json b/homeassistant/components/melnor/translations/ca.json new file mode 100644 index 00000000000..3aa06dfddc6 --- /dev/null +++ b/homeassistant/components/melnor/translations/ca.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "No hi ha cap dispositiu Melnor Bluetooth a prop." + }, + "step": { + "bluetooth_confirm": { + "description": "Vols afegir la v\u00e0lvula Melnor Bluetooth `{name}` a Home Assistant?", + "title": "S'ha descobert una v\u00e0lvula Melnor Bluetooth" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/melnor/translations/de.json b/homeassistant/components/melnor/translations/de.json new file mode 100644 index 00000000000..5792062dd87 --- /dev/null +++ b/homeassistant/components/melnor/translations/de.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "In der N\u00e4he gibt es keine Melnor Bluetooth-Ger\u00e4te." + }, + "step": { + "bluetooth_confirm": { + "description": "M\u00f6chtest du das Melnor Bluetooth-Ventil `{name}` zu Home Assistant hinzuf\u00fcgen?", + "title": "Melnor Bluetooth-Ventil entdeckt" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/melnor/translations/el.json b/homeassistant/components/melnor/translations/el.json new file mode 100644 index 00000000000..7380a05cb4d --- /dev/null +++ b/homeassistant/components/melnor/translations/el.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u0394\u03b5\u03bd \u03c5\u03c0\u03ac\u03c1\u03c7\u03bf\u03c5\u03bd \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 Melnor Bluetooth \u03ba\u03bf\u03bd\u03c4\u03ac \u03c3\u03b1\u03c2." + }, + "step": { + "bluetooth_confirm": { + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c0\u03c1\u03bf\u03c3\u03b8\u03ad\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03b2\u03b1\u03bb\u03b2\u03af\u03b4\u03b1 Bluetooth Melnor `{name}` \u03c3\u03c4\u03bf Home Assistant;", + "title": "\u0391\u03bd\u03b1\u03ba\u03b1\u03bb\u03cd\u03c6\u03b8\u03b7\u03ba\u03b5 \u03b7 \u03b2\u03b1\u03bb\u03b2\u03af\u03b4\u03b1 Bluetooth Melnor" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/melnor/translations/es.json b/homeassistant/components/melnor/translations/es.json new file mode 100644 index 00000000000..32ed43930db --- /dev/null +++ b/homeassistant/components/melnor/translations/es.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "No hay ning\u00fan dispositivo Bluetooth Melnor cerca." + }, + "step": { + "bluetooth_confirm": { + "description": "\u00bfQuieres a\u00f1adir la v\u00e1lvula Bluetooth Melnor `{name}` a Home Assistant?", + "title": "Descubierta v\u00e1lvula Bluetooth Melnor" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/melnor/translations/et.json b/homeassistant/components/melnor/translations/et.json new file mode 100644 index 00000000000..12a75835d26 --- /dev/null +++ b/homeassistant/components/melnor/translations/et.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "L\u00e4heduses pole \u00fchtegi Melnor Bluetooth-seadet." + }, + "step": { + "bluetooth_confirm": { + "description": "Kas lisada Melnor Bluetooth-klapp '{name}' Home Assistantisse?", + "title": "Leiti Melnor Bluetooth klapp" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/melnor/translations/fr.json b/homeassistant/components/melnor/translations/fr.json new file mode 100644 index 00000000000..9c5bdc781a4 --- /dev/null +++ b/homeassistant/components/melnor/translations/fr.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "Il n'y a aucun appareil Bluetooth Melnor \u00e0 proximit\u00e9." + }, + "step": { + "bluetooth_confirm": { + "description": "Voulez-vous ajouter la valve Bluetooth Melnor `{name}` \u00e0 Home Assistant\u00a0?", + "title": "Valve Bluetooth Melnor d\u00e9couverte" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/melnor/translations/hu.json b/homeassistant/components/melnor/translations/hu.json new file mode 100644 index 00000000000..9f96c1bbaae --- /dev/null +++ b/homeassistant/components/melnor/translations/hu.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "Nincsenek Melnor Bluetooth-eszk\u00f6z\u00f6k a k\u00f6zelben." + }, + "step": { + "bluetooth_confirm": { + "description": "Szeretn\u00e9 hozz\u00e1adni a Melnor Bluetooth eszk\u00f6zt \"{name}\" Home Assistant-hoz?", + "title": "Felfedezett Melnor Bluetooth szelep" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/melnor/translations/id.json b/homeassistant/components/melnor/translations/id.json new file mode 100644 index 00000000000..32e1b5bdd94 --- /dev/null +++ b/homeassistant/components/melnor/translations/id.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "Tidak ada perangkat Bluetooth Melnor di sekitar." + }, + "step": { + "bluetooth_confirm": { + "description": "Ingin menambahkan katup Bluetooth Melnor `{name}` ke Home Assistant?", + "title": "Katup Bluetooth Melnor yang ditemukan" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/melnor/translations/it.json b/homeassistant/components/melnor/translations/it.json new file mode 100644 index 00000000000..9124fdfce13 --- /dev/null +++ b/homeassistant/components/melnor/translations/it.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "Non ci sono dispositivi Melnor Bluetooth nelle vicinanze." + }, + "step": { + "bluetooth_confirm": { + "description": "Vuoi aggiungere la valvola Bluetooth Melnor `{name}` a Home Assistant?", + "title": "Scoperta la valvola Bluetooth Melnor" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/melnor/translations/ja.json b/homeassistant/components/melnor/translations/ja.json new file mode 100644 index 00000000000..5fe71d5781c --- /dev/null +++ b/homeassistant/components/melnor/translations/ja.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "Melnor Bluetooth\u30c7\u30d0\u30a4\u30b9\u304c\u8fd1\u304f\u306b\u3042\u308a\u307e\u305b\u3093\u3002" + }, + "step": { + "bluetooth_confirm": { + "description": "Melnor Bluetooth\u30d0\u30eb\u30d6 `{name}` \u3092\u3001Home Assistan\u306b\u8ffd\u52a0\u3057\u307e\u3059\u304b\uff1f", + "title": "Melnor Bluetooth\u30d0\u30eb\u30d6\u3092\u767a\u898b" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/melnor/translations/nl.json b/homeassistant/components/melnor/translations/nl.json new file mode 100644 index 00000000000..14ebeed502e --- /dev/null +++ b/homeassistant/components/melnor/translations/nl.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "no_devices_found": "Er zijn geen Melnor Bluetooth-apparaten in de buurt." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/melnor/translations/no.json b/homeassistant/components/melnor/translations/no.json new file mode 100644 index 00000000000..98c873ad9fe --- /dev/null +++ b/homeassistant/components/melnor/translations/no.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "Det er ingen Melnor Bluetooth-enheter i n\u00e6rheten." + }, + "step": { + "bluetooth_confirm": { + "description": "Vil du legge til Melnor Bluetooth-ventilen ` {name} ` til Home Assistant?", + "title": "Oppdaget Melnor Bluetooth-ventil" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/melnor/translations/pl.json b/homeassistant/components/melnor/translations/pl.json new file mode 100644 index 00000000000..707f4d67873 --- /dev/null +++ b/homeassistant/components/melnor/translations/pl.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "W pobli\u017cu nie ma \u017cadnych urz\u0105dze\u0144 Melnor Bluetooth." + }, + "step": { + "bluetooth_confirm": { + "description": "Czy chcesz doda\u0107 zaw\u00f3r Melnor Bluetooth o nazwie `{name}` do Home Assistanta?", + "title": "Wykryty zaw\u00f3r Melnor Bluetooth" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/melnor/translations/pt.json b/homeassistant/components/melnor/translations/pt.json new file mode 100644 index 00000000000..2c44c3380bc --- /dev/null +++ b/homeassistant/components/melnor/translations/pt.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "N\u00e3o existem dispositivos Bluetooth Melnor nas proximidades." + }, + "step": { + "bluetooth_confirm": { + "description": "Deseja adicionar a v\u00e1lvula Melnor Bluetooth ` {name} ` ao Home Assistant?", + "title": "V\u00e1lvula Bluetooth Melnor descoberta" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/melnor/translations/ru.json b/homeassistant/components/melnor/translations/ru.json new file mode 100644 index 00000000000..b6e0488ce38 --- /dev/null +++ b/homeassistant/components/melnor/translations/ru.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "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": { + "bluetooth_confirm": { + "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u043a\u043b\u0430\u043f\u0430\u043d Melnor Bluetooth `{name}`?", + "title": "\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d \u043a\u043b\u0430\u043f\u0430\u043d Melnor Bluetooth" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/melnor/translations/sv.json b/homeassistant/components/melnor/translations/sv.json new file mode 100644 index 00000000000..0f7f7b8c06d --- /dev/null +++ b/homeassistant/components/melnor/translations/sv.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "Det finns inga Melnor Bluetooth-enheter i n\u00e4rheten." + }, + "step": { + "bluetooth_confirm": { + "description": "Vill du l\u00e4gga till Melnor Bluetooth-ventilen ` {name} ` till Home Assistant?", + "title": "Uppt\u00e4ckte Melnor Bluetooth-ventil" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/melnor/translations/tr.json b/homeassistant/components/melnor/translations/tr.json new file mode 100644 index 00000000000..7c99d8ab655 --- /dev/null +++ b/homeassistant/components/melnor/translations/tr.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "Yak\u0131nlarda Melnor Bluetooth cihaz\u0131 yok." + }, + "step": { + "bluetooth_confirm": { + "description": "Home Assistant'a Melnor Bluetooth valfi ` {name} ` eklemek ister misiniz?", + "title": "Melnor Bluetooth valfi ke\u015ffedildi" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/melnor/translations/zh-Hant.json b/homeassistant/components/melnor/translations/zh-Hant.json new file mode 100644 index 00000000000..0374906bb4b --- /dev/null +++ b/homeassistant/components/melnor/translations/zh-Hant.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u9644\u8fd1\u6c92\u6709\u4efb\u4f55 Melnor \u85cd\u7259\u88dd\u7f6e\u3002" + }, + "step": { + "bluetooth_confirm": { + "description": "\u662f\u5426\u8981\u5c07 Melnor Bluetooth valve `{name}`\u65b0\u589e\u81f3 Home Assistant\uff1f", + "title": "\u81ea\u52d5\u641c\u7d22\u5230\u7684 Melnor Bluetooth valve" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/met/__init__.py b/homeassistant/components/met/__init__.py index b4889a62411..2857c057482 100644 --- a/homeassistant/components/met/__init__.py +++ b/homeassistant/components/met/__init__.py @@ -24,8 +24,8 @@ from homeassistant.core import Event, HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from homeassistant.util.distance import convert as convert_distance -import homeassistant.util.dt as dt_util +from homeassistant.util import dt as dt_util +from homeassistant.util.unit_conversion import DistanceConverter from .const import ( CONF_TRACK_HOME, @@ -160,7 +160,7 @@ class MetWeatherData: if not self._is_metric: elevation = int( - round(convert_distance(elevation, LENGTH_FEET, LENGTH_METERS)) + round(DistanceConverter.convert(elevation, LENGTH_FEET, LENGTH_METERS)) ) coordinates = { diff --git a/homeassistant/components/met/config_flow.py b/homeassistant/components/met/config_flow.py index 6c4c4d33d5b..baf7269a81d 100644 --- a/homeassistant/components/met/config_flow.py +++ b/homeassistant/components/met/config_flow.py @@ -1,11 +1,14 @@ """Config flow to configure Met component.""" from __future__ import annotations +from typing import Any + import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback +from homeassistant.data_entry_flow import FlowResult import homeassistant.helpers.config_validation as cv from .const import ( @@ -18,7 +21,7 @@ from .const import ( @callback -def configured_instances(hass): +def configured_instances(hass: HomeAssistant) -> set[str]: """Return a set of configured SimpliSafe instances.""" entries = [] for entry in hass.config_entries.async_entries(DOMAIN): @@ -36,11 +39,13 @@ class MetFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 - def __init__(self): + def __init__(self) -> None: """Init MetFlowHandler.""" - self._errors = {} + self._errors: dict[str, Any] = {} - async def async_step_user(self, user_input=None): + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle a flow initialized by the user.""" self._errors = {} @@ -62,8 +67,12 @@ class MetFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) async def _show_config_form( - self, name=None, latitude=None, longitude=None, elevation=None - ): + self, + name: str | None = None, + latitude: float | None = None, + longitude: float | None = None, + elevation: int | None = None, + ) -> FlowResult: """Show the configuration form to edit location data.""" return self.async_show_form( step_id="user", @@ -78,7 +87,9 @@ class MetFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors=self._errors, ) - async def async_step_onboarding(self, data=None): + async def async_step_onboarding( + self, data: dict[str, Any] | None = None + ) -> FlowResult: """Handle a flow initialized by onboarding.""" # Don't create entry if latitude or longitude isn't set. # Also, filters out our onboarding default location. diff --git a/homeassistant/components/meteoalarm/binary_sensor.py b/homeassistant/components/meteoalarm/binary_sensor.py index c4b6e49a636..246074ce585 100644 --- a/homeassistant/components/meteoalarm/binary_sensor.py +++ b/homeassistant/components/meteoalarm/binary_sensor.py @@ -74,15 +74,14 @@ class MeteoAlertBinarySensor(BinarySensorEntity): self._attr_name = name self._api = api - def update(self): + def update(self) -> None: """Update device state.""" - self._attr_extra_state_attributes = None + self._attr_extra_state_attributes = {} self._attr_is_on = False if alert := self._api.get_alert(): expiration_date = dt_util.parse_datetime(alert["expires"]) - now = dt_util.utcnow() - if expiration_date > now: + if expiration_date is not None and expiration_date > dt_util.utcnow(): self._attr_extra_state_attributes = alert self._attr_is_on = True diff --git a/homeassistant/components/meteoclimatic/translations/cs.json b/homeassistant/components/meteoclimatic/translations/cs.json index 3b814303e69..c9f0e950664 100644 --- a/homeassistant/components/meteoclimatic/translations/cs.json +++ b/homeassistant/components/meteoclimatic/translations/cs.json @@ -1,7 +1,11 @@ { "config": { "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "error": { + "not_found": "V s\u00edti nebyla nalezena \u017e\u00e1dn\u00e1 za\u0159\u00edzen\u00ed" } } } \ No newline at end of file diff --git a/homeassistant/components/metoffice/__init__.py b/homeassistant/components/metoffice/__init__.py index e71c417da43..057947d76e4 100644 --- a/homeassistant/components/metoffice/__init__.py +++ b/homeassistant/components/metoffice/__init__.py @@ -1,7 +1,10 @@ """The Met Office integration.""" +from __future__ import annotations import asyncio import logging +import re +from typing import Any import datapoint @@ -13,8 +16,9 @@ from homeassistant.const import ( CONF_NAME, Platform, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import entity_registry from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import DataUpdateCoordinator @@ -45,6 +49,45 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: api_key = entry.data[CONF_API_KEY] site_name = entry.data[CONF_NAME] + coordinates = f"{latitude}_{longitude}" + + @callback + def update_unique_id( + entity_entry: entity_registry.RegistryEntry, + ) -> dict[str, Any] | None: + """Update unique ID of entity entry.""" + + if entity_entry.domain != Platform.SENSOR: + return None + + name_to_key = { + "Station Name": "name", + "Weather": "weather", + "Temperature": "temperature", + "Feels Like Temperature": "feels_like_temperature", + "Wind Speed": "wind_speed", + "Wind Direction": "wind_direction", + "Wind Gust": "wind_gust", + "Visibility": "visibility", + "Visibility Distance": "visibility_distance", + "UV Index": "uv", + "Probability of Precipitation": "precipitation", + "Humidity": "humidity", + } + + match = re.search(f"(?P.*)_{coordinates}.*", entity_entry.unique_id) + + if match is None: + return None + + if (name := match.group("name")) in name_to_key: + return { + "new_unique_id": entity_entry.unique_id.replace(name, name_to_key[name]) + } + return None + + await entity_registry.async_migrate_entries(hass, entry.entry_id, update_unique_id) + connection = datapoint.connection(api_key=api_key) site = await hass.async_add_executor_job( @@ -84,7 +127,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: METOFFICE_HOURLY_COORDINATOR: metoffice_hourly_coordinator, METOFFICE_DAILY_COORDINATOR: metoffice_daily_coordinator, METOFFICE_NAME: site_name, - METOFFICE_COORDINATES: f"{latitude}_{longitude}", + METOFFICE_COORDINATES: coordinates, } # Fetch initial data so we have data when entities subscribe diff --git a/homeassistant/components/metoffice/const.py b/homeassistant/components/metoffice/const.py index 12f88cc6d56..e4843d1235e 100644 --- a/homeassistant/components/metoffice/const.py +++ b/homeassistant/components/metoffice/const.py @@ -33,9 +33,7 @@ METOFFICE_MONITORED_CONDITIONS = "metoffice_monitored_conditions" METOFFICE_NAME = "metoffice_name" MODE_3HOURLY = "3hourly" -MODE_3HOURLY_LABEL = "3-Hourly" MODE_DAILY = "daily" -MODE_DAILY_LABEL = "Daily" CONDITION_CLASSES: dict[str, list[str]] = { ATTR_CONDITION_CLEAR_NIGHT: ["0"], diff --git a/homeassistant/components/metoffice/sensor.py b/homeassistant/components/metoffice/sensor.py index e24e2299be4..77532b379b6 100644 --- a/homeassistant/components/metoffice/sensor.py +++ b/homeassistant/components/metoffice/sensor.py @@ -35,9 +35,7 @@ from .const import ( METOFFICE_DAILY_COORDINATOR, METOFFICE_HOURLY_COORDINATOR, METOFFICE_NAME, - MODE_3HOURLY_LABEL, MODE_DAILY, - MODE_DAILY_LABEL, VISIBILITY_CLASSES, VISIBILITY_DISTANCE_CLASSES, ) @@ -52,7 +50,7 @@ ATTR_SITE_NAME = "site_name" SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="name", - name="Station Name", + name="Station name", device_class=None, native_unit_of_measurement=None, icon="mdi:label-outline", @@ -76,7 +74,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( ), SensorEntityDescription( key="feels_like_temperature", - name="Feels Like Temperature", + name="Feels like temperature", device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=TEMP_CELSIUS, icon=None, @@ -84,25 +82,24 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( ), SensorEntityDescription( key="wind_speed", - name="Wind Speed", - device_class=None, + name="Wind speed", native_unit_of_measurement=SPEED_MILES_PER_HOUR, + device_class=SensorDeviceClass.SPEED, icon="mdi:weather-windy", entity_registry_enabled_default=True, ), SensorEntityDescription( key="wind_direction", - name="Wind Direction", - device_class=None, + name="Wind direction", native_unit_of_measurement=None, icon="mdi:compass-outline", entity_registry_enabled_default=False, ), SensorEntityDescription( key="wind_gust", - name="Wind Gust", - device_class=None, + name="Wind gust", native_unit_of_measurement=SPEED_MILES_PER_HOUR, + device_class=SensorDeviceClass.SPEED, icon="mdi:weather-windy", entity_registry_enabled_default=False, ), @@ -116,15 +113,15 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( ), SensorEntityDescription( key="visibility_distance", - name="Visibility Distance", - device_class=None, + name="Visibility distance", native_unit_of_measurement=LENGTH_KILOMETERS, + device_class=SensorDeviceClass.DISTANCE, icon="mdi:eye", entity_registry_enabled_default=False, ), SensorEntityDescription( key="uv", - name="UV Index", + name="UV index", device_class=None, native_unit_of_measurement=UV_INDEX, icon="mdi:weather-sunny-alert", @@ -132,7 +129,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( ), SensorEntityDescription( key="precipitation", - name="Probability of Precipitation", + name="Probability of precipitation", device_class=None, native_unit_of_measurement=PERCENTAGE, icon="mdi:weather-rainy", @@ -183,6 +180,8 @@ class MetOfficeCurrentSensor( ): """Implementation of a Met Office current weather condition sensor.""" + _attr_has_entity_name = True + def __init__( self, coordinator: DataUpdateCoordinator[MetOfficeData], @@ -194,13 +193,13 @@ class MetOfficeCurrentSensor( super().__init__(coordinator) self.entity_description = description - mode_label = MODE_3HOURLY_LABEL if use_3hourly else MODE_DAILY_LABEL + mode_label = "3-hourly" if use_3hourly else "daily" self._attr_device_info = get_device_info( coordinates=hass_data[METOFFICE_COORDINATES], name=hass_data[METOFFICE_NAME] ) - self._attr_name = f"{hass_data[METOFFICE_NAME]} {description.name} {mode_label}" - self._attr_unique_id = f"{description.name}_{hass_data[METOFFICE_COORDINATES]}" + self._attr_name = f"{description.name} {mode_label}" + self._attr_unique_id = f"{description.key}_{hass_data[METOFFICE_COORDINATES]}" if not use_3hourly: self._attr_unique_id = f"{self._attr_unique_id}_{MODE_DAILY}" self._attr_entity_registry_enabled_default = ( diff --git a/homeassistant/components/metoffice/weather.py b/homeassistant/components/metoffice/weather.py index 184782d4c12..4733ca6ea73 100644 --- a/homeassistant/components/metoffice/weather.py +++ b/homeassistant/components/metoffice/weather.py @@ -27,15 +27,12 @@ from . import get_device_info from .const import ( ATTRIBUTION, CONDITION_CLASSES, - DEFAULT_NAME, DOMAIN, METOFFICE_COORDINATES, METOFFICE_DAILY_COORDINATOR, METOFFICE_HOURLY_COORDINATOR, METOFFICE_NAME, - MODE_3HOURLY_LABEL, MODE_DAILY, - MODE_DAILY_LABEL, ) from .data import MetOfficeData @@ -83,6 +80,7 @@ class MetOfficeWeather( """Implementation of a Met Office weather condition.""" _attr_attribution = ATTRIBUTION + _attr_has_entity_name = True _attr_native_temperature_unit = TEMP_CELSIUS _attr_native_pressure_unit = PRESSURE_HPA @@ -97,11 +95,10 @@ class MetOfficeWeather( """Initialise the platform with a data instance.""" super().__init__(coordinator) - mode_label = MODE_3HOURLY_LABEL if use_3hourly else MODE_DAILY_LABEL self._attr_device_info = get_device_info( coordinates=hass_data[METOFFICE_COORDINATES], name=hass_data[METOFFICE_NAME] ) - self._attr_name = f"{DEFAULT_NAME} {hass_data[METOFFICE_NAME]} {mode_label}" + self._attr_name = "3-Hourly" if use_3hourly else "Daily" self._attr_unique_id = hass_data[METOFFICE_COORDINATES] if not use_3hourly: self._attr_unique_id = f"{self._attr_unique_id}_{MODE_DAILY}" diff --git a/homeassistant/components/mfi/sensor.py b/homeassistant/components/mfi/sensor.py index 34a56641d2d..de7e661d9d2 100644 --- a/homeassistant/components/mfi/sensor.py +++ b/homeassistant/components/mfi/sensor.py @@ -143,6 +143,6 @@ class MfiSensor(SensorEntity): return "State" return tag - def update(self): + def update(self) -> None: """Get the latest data.""" self._port.refresh() diff --git a/homeassistant/components/mfi/switch.py b/homeassistant/components/mfi/switch.py index 76a55e46ba1..a25d0dfc87b 100644 --- a/homeassistant/components/mfi/switch.py +++ b/homeassistant/components/mfi/switch.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any from mficlient.client import FailedToLogin, MFiClient import requests @@ -94,19 +95,19 @@ class MfiSwitch(SwitchEntity): """Return true if the device is on.""" return self._port.output - def update(self): + def update(self) -> None: """Get the latest state and update the state.""" self._port.refresh() if self._target_state is not None: self._port.data["output"] = float(self._target_state) self._target_state = None - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" self._port.control(True) self._target_state = True - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the switch off.""" self._port.control(False) self._target_state = False diff --git a/homeassistant/components/microsoft/tts.py b/homeassistant/components/microsoft/tts.py index 840b35c2f85..7deb8f27c68 100644 --- a/homeassistant/components/microsoft/tts.py +++ b/homeassistant/components/microsoft/tts.py @@ -166,6 +166,16 @@ class MicrosoftProvider(Provider): """Return list of supported languages.""" return SUPPORTED_LANGUAGES + @property + def supported_options(self): + """Return list of supported options like voice, emotion.""" + return [CONF_GENDER, CONF_TYPE] + + @property + def default_options(self): + """Return a dict include default options.""" + return {CONF_GENDER: self._gender, CONF_TYPE: self._type} + def get_tts_audio(self, message, language, options=None): """Load TTS from Microsoft.""" if language is None: @@ -175,8 +185,8 @@ class MicrosoftProvider(Provider): trans = pycsspeechtts.TTSTranslator(self._apikey, self._region) data = trans.speak( language=language, - gender=self._gender, - voiceType=self._type, + gender=options[CONF_GENDER], + voiceType=options[CONF_TYPE], output=self._output, rate=self._rate, volume=self._volume, diff --git a/homeassistant/components/miflora/translations/bg.json b/homeassistant/components/miflora/translations/bg.json new file mode 100644 index 00000000000..bc09e4fc74d --- /dev/null +++ b/homeassistant/components/miflora/translations/bg.json @@ -0,0 +1,7 @@ +{ + "issues": { + "replaced": { + "title": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u043d\u0430 Mi Flora \u0435 \u0437\u0430\u043c\u0435\u043d\u0435\u043d\u0430" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mikrotik/config_flow.py b/homeassistant/components/mikrotik/config_flow.py index d506c2c75e4..ed62734578f 100644 --- a/homeassistant/components/mikrotik/config_flow.py +++ b/homeassistant/components/mikrotik/config_flow.py @@ -8,7 +8,6 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import ( CONF_HOST, - CONF_NAME, CONF_PASSWORD, CONF_PORT, CONF_USERNAME, @@ -49,12 +48,7 @@ class MikrotikFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a flow initialized by the user.""" errors = {} if user_input is not None: - for entry in self._async_current_entries(): - if entry.data[CONF_HOST] == user_input[CONF_HOST]: - return self.async_abort(reason="already_configured") - if entry.data[CONF_NAME] == user_input[CONF_NAME]: - errors[CONF_NAME] = "name_exists" - break + self._async_abort_entries_match({CONF_HOST: user_input[CONF_HOST]}) try: await self.hass.async_add_executor_job(get_api, user_input) @@ -66,13 +60,12 @@ class MikrotikFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if not errors: return self.async_create_entry( - title=user_input[CONF_NAME], data=user_input + title=f"{DEFAULT_NAME} ({user_input[CONF_HOST]})", data=user_input ) return self.async_show_form( step_id="user", data_schema=vol.Schema( { - vol.Required(CONF_NAME, default=DEFAULT_NAME): str, vol.Required(CONF_HOST): str, vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str, diff --git a/homeassistant/components/mikrotik/device.py b/homeassistant/components/mikrotik/device.py index f37ef6fee80..bf3cb47adc3 100644 --- a/homeassistant/components/mikrotik/device.py +++ b/homeassistant/components/mikrotik/device.py @@ -25,7 +25,7 @@ class Device: @property def name(self) -> str: """Return device name.""" - return self._params.get("host-name", self.mac) + return str(self._params.get("host-name", self.mac)) @property def ip_address(self) -> str | None: diff --git a/homeassistant/components/mikrotik/device_tracker.py b/homeassistant/components/mikrotik/device_tracker.py index f50c49d5ab6..9529322affd 100644 --- a/homeassistant/components/mikrotik/device_tracker.py +++ b/homeassistant/components/mikrotik/device_tracker.py @@ -3,11 +3,8 @@ from __future__ import annotations from typing import Any +from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER, SourceType from homeassistant.components.device_tracker.config_entry import ScannerEntity -from homeassistant.components.device_tracker.const import ( - DOMAIN as DEVICE_TRACKER, - SourceType, -) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry @@ -86,7 +83,7 @@ class MikrotikDataUpdateCoordinatorTracker( """Initialize the tracked device.""" super().__init__(coordinator) self.device = device - self._attr_name = str(device.name) + self._attr_name = device.name self._attr_unique_id = device.mac @property diff --git a/homeassistant/components/mill/climate.py b/homeassistant/components/mill/climate.py index e7cd297bd59..84f87e79a2d 100644 --- a/homeassistant/components/mill/climate.py +++ b/homeassistant/components/mill/climate.py @@ -1,11 +1,13 @@ """Support for mill wifi-enabled home heaters.""" +from typing import Any + import mill import voluptuous as vol -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( FAN_OFF, FAN_ON, + ClimateEntity, ClimateEntityFeature, HVACAction, HVACMode, @@ -15,6 +17,7 @@ from homeassistant.const import ( ATTR_TEMPERATURE, CONF_IP_ADDRESS, CONF_USERNAME, + PRECISION_HALVES, PRECISION_WHOLE, TEMP_CELSIUS, ) @@ -89,7 +92,6 @@ class MillHeater(CoordinatorEntity, ClimateEntity): _attr_fan_modes = [FAN_ON, FAN_OFF] _attr_max_temp = MAX_TEMP _attr_min_temp = MIN_TEMP - _attr_target_temperature_step = PRECISION_WHOLE _attr_temperature_unit = TEMP_CELSIUS def __init__(self, coordinator, heater): @@ -117,21 +119,23 @@ class MillHeater(CoordinatorEntity, ClimateEntity): self._attr_supported_features = ( ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE ) + self._attr_target_temperature_step = PRECISION_WHOLE else: self._attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE + self._attr_target_temperature_step = PRECISION_HALVES self._update_attr(heater) - async def async_set_temperature(self, **kwargs): + async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None: return await self.coordinator.mill_data_connection.set_heater_temp( - self._id, int(temperature) + self._id, float(temperature) ) await self.coordinator.async_request_refresh() - async def async_set_fan_mode(self, fan_mode): + async def async_set_fan_mode(self, fan_mode: str) -> None: """Set new target fan mode.""" fan_status = 1 if fan_mode == FAN_ON else 0 await self.coordinator.mill_data_connection.heater_control( @@ -200,7 +204,7 @@ class LocalMillHeater(CoordinatorEntity, ClimateEntity): _attr_max_temp = MAX_TEMP _attr_min_temp = MIN_TEMP _attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE - _attr_target_temperature_step = PRECISION_WHOLE + _attr_target_temperature_step = PRECISION_HALVES _attr_temperature_unit = TEMP_CELSIUS def __init__(self, coordinator): @@ -220,12 +224,12 @@ class LocalMillHeater(CoordinatorEntity, ClimateEntity): self._update_attr() - async def async_set_temperature(self, **kwargs): + async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None: return await self.coordinator.mill_data_connection.set_target_temperature( - int(temperature) + float(temperature) ) await self.coordinator.async_request_refresh() diff --git a/homeassistant/components/mill/manifest.json b/homeassistant/components/mill/manifest.json index c2adebae594..432c77601d1 100644 --- a/homeassistant/components/mill/manifest.json +++ b/homeassistant/components/mill/manifest.json @@ -2,7 +2,7 @@ "domain": "mill", "name": "Mill", "documentation": "https://www.home-assistant.io/integrations/mill", - "requirements": ["millheater==0.9.0", "mill-local==0.1.1"], + "requirements": ["millheater==0.10.0", "mill-local==0.2.0"], "codeowners": ["@danielhiversen"], "config_flow": true, "iot_class": "local_polling", diff --git a/homeassistant/components/mill/translations/cs.json b/homeassistant/components/mill/translations/cs.json index f6c47b4c840..10f846dc59a 100644 --- a/homeassistant/components/mill/translations/cs.json +++ b/homeassistant/components/mill/translations/cs.json @@ -5,6 +5,19 @@ }, "error": { "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" + }, + "step": { + "cloud": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + }, + "local": { + "data": { + "ip_address": "IP adresa" + } + } } } } \ 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 2114a5406d0..0fed67f15b9 100644 --- a/homeassistant/components/min_max/config_flow.py +++ b/homeassistant/components/min_max/config_flow.py @@ -22,6 +22,7 @@ _STATISTIC_MEASURES = [ 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"), ] diff --git a/homeassistant/components/min_max/sensor.py b/homeassistant/components/min_max/sensor.py index c5a51cdda7a..0f53875861d 100644 --- a/homeassistant/components/min_max/sensor.py +++ b/homeassistant/components/min_max/sensor.py @@ -39,6 +39,7 @@ ATTR_MEAN = "mean" ATTR_MEDIAN = "median" ATTR_LAST = "last" ATTR_LAST_ENTITY_ID = "last_entity_id" +ATTR_RANGE = "range" ICON = "mdi:calculator" @@ -48,6 +49,7 @@ SENSOR_TYPES = { ATTR_MEAN: "mean", ATTR_MEDIAN: "median", ATTR_LAST: "last", + ATTR_RANGE: "range", } SENSOR_TYPE_TO_ATTR = {v: k for k, v in SENSOR_TYPES.items()} @@ -158,6 +160,19 @@ def calc_median(sensor_values, round_digits): return round(statistics.median(result), round_digits) +def calc_range(sensor_values, round_digits): + """Calculate range value, honoring unknown states.""" + result = [ + sensor_value + for _, sensor_value in sensor_values + if sensor_value not in [STATE_UNKNOWN, STATE_UNAVAILABLE] + ] + + if not result: + return None + return round(max(result) - min(result), round_digits) + + class MinMaxSensor(SensorEntity): """Representation of a min/max sensor.""" @@ -180,11 +195,12 @@ class MinMaxSensor(SensorEntity): self._unit_of_measurement = None self._unit_of_measurement_mismatch = False self.min_value = self.max_value = self.mean = self.last = self.median = None + self.range = None self.min_entity_id = self.max_entity_id = self.last_entity_id = None self.count_sensors = len(self._entity_ids) self.states = {} - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Handle added to Hass.""" self.async_on_remove( async_track_state_change_event( @@ -288,3 +304,4 @@ class MinMaxSensor(SensorEntity): self.max_entity_id, self.max_value = calc_max(sensor_values) self.mean = calc_mean(sensor_values, self._round_digits) self.median = calc_median(sensor_values, self._round_digits) + self.range = calc_range(sensor_values, self._round_digits) diff --git a/homeassistant/components/min_max/translations/ja.json b/homeassistant/components/min_max/translations/ja.json index 072e7a9b487..c4d33b616b8 100644 --- a/homeassistant/components/min_max/translations/ja.json +++ b/homeassistant/components/min_max/translations/ja.json @@ -11,7 +11,7 @@ "data_description": { "round_digits": "\u7d71\u8a08\u7684\u7279\u6027\u304c\u5e73\u5747\u5024\u307e\u305f\u306f\u4e2d\u592e\u5024\u306e\u5834\u5408\u306b\u3001\u51fa\u529b\u306b\u542b\u307e\u308c\u308b\u5c0f\u6570\u70b9\u4ee5\u4e0b\u306e\u6841\u6570\u3092\u5236\u5fa1\u3057\u307e\u3059\u3002" }, - "description": "\u7d71\u8a08\u7684\u7279\u6027\u304c\u5e73\u5747\u307e\u305f\u306f\u4e2d\u592e\u5024\u306a\u5834\u5408\u306e\u7cbe\u5ea6\u3067\u3001\u5c0f\u6570\u70b9\u4ee5\u4e0b\u306e\u6841\u6570\u3092\u5236\u5fa1\u3057\u307e\u3059\u3002", + "description": "\u5165\u529b\u30bb\u30f3\u30b5\u30fc\u306e\u30ea\u30b9\u30c8\u304b\u3089\u6700\u5c0f\u5024\u3001\u6700\u5927\u5024\u3001\u5e73\u5747\u5024\u3001\u307e\u305f\u306f\u4e2d\u592e\u5024\u3092\u8a08\u7b97\u3059\u308b\u30bb\u30f3\u30b5\u30fc\u3092\u4f5c\u6210\u3057\u307e\u3059\u3002", "title": "\u6700\u5c0f/\u6700\u5927/\u5e73\u5747/\u4e2d\u592e\u5024\u30bb\u30f3\u30b5\u30fc\u3092\u8ffd\u52a0" } } diff --git a/homeassistant/components/mjpeg/translations/cs.json b/homeassistant/components/mjpeg/translations/cs.json index 9c6676509bf..f5813b43ac0 100644 --- a/homeassistant/components/mjpeg/translations/cs.json +++ b/homeassistant/components/mjpeg/translations/cs.json @@ -10,19 +10,25 @@ "step": { "user": { "data": { - "name": "Jm\u00e9no" + "name": "Jm\u00e9no", + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" } } } }, "options": { "error": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed" }, "step": { "init": { "data": { - "name": "Jm\u00e9no" + "name": "Jm\u00e9no", + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" } } } diff --git a/homeassistant/components/moat/translations/bg.json b/homeassistant/components/moat/translations/bg.json new file mode 100644 index 00000000000..a61dac839ad --- /dev/null +++ b/homeassistant/components/moat/translations/bg.json @@ -0,0 +1,20 @@ +{ + "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" + }, + "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/moat/translations/cs.json b/homeassistant/components/moat/translations/cs.json new file mode 100644 index 00000000000..925c1cbd337 --- /dev/null +++ b/homeassistant/components/moat/translations/cs.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "already_in_progress": "Konfigurace ji\u017e prob\u00edh\u00e1", + "no_devices_found": "V s\u00edti nebyla nalezena \u017e\u00e1dn\u00e1 za\u0159\u00edzen\u00ed" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Chcete nastavit {name}?" + }, + "user": { + "data": { + "address": "Za\u0159\u00edzen\u00ed" + }, + "description": "Zvolte za\u0159\u00edzen\u00ed, kter\u00e9 chcete nastavit" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mobile_app/device_tracker.py b/homeassistant/components/mobile_app/device_tracker.py index d0f1db6caff..6f9c68224ec 100644 --- a/homeassistant/components/mobile_app/device_tracker.py +++ b/homeassistant/components/mobile_app/device_tracker.py @@ -4,9 +4,9 @@ from homeassistant.components.device_tracker import ( ATTR_GPS, ATTR_GPS_ACCURACY, ATTR_LOCATION_NAME, + SourceType, ) from homeassistant.components.device_tracker.config_entry import TrackerEntity -from homeassistant.components.device_tracker.const import SourceType from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_BATTERY_LEVEL, @@ -112,7 +112,7 @@ class MobileAppEntity(TrackerEntity, RestoreEntity): """Return the device info.""" return device_info(self._entry.data) - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Call when entity about to be added to Home Assistant.""" await super().async_added_to_hass() self._dispatch_unsub = async_dispatcher_connect( @@ -138,7 +138,7 @@ class MobileAppEntity(TrackerEntity, RestoreEntity): data.update({key: attr[key] for key in attr if key in ATTR_KEYS}) self._data = data - async def async_will_remove_from_hass(self): + async def async_will_remove_from_hass(self) -> None: """Call when entity is being removed from hass.""" await super().async_will_remove_from_hass() diff --git a/homeassistant/components/mobile_app/logbook.py b/homeassistant/components/mobile_app/logbook.py index 6f7c2e4e99c..083db294a1c 100644 --- a/homeassistant/components/mobile_app/logbook.py +++ b/homeassistant/components/mobile_app/logbook.py @@ -3,7 +3,7 @@ from __future__ import annotations from collections.abc import Callable -from homeassistant.components.logbook.const import ( +from homeassistant.components.logbook import ( LOGBOOK_ENTRY_ENTITY_ID, LOGBOOK_ENTRY_ICON, LOGBOOK_ENTRY_MESSAGE, diff --git a/homeassistant/components/mobile_app/webhook.py b/homeassistant/components/mobile_app/webhook.py index 61b69ebad5c..7c6bcc58db9 100644 --- a/homeassistant/components/mobile_app/webhook.py +++ b/homeassistant/components/mobile_app/webhook.py @@ -27,7 +27,7 @@ from homeassistant.components.sensor import ( DEVICE_CLASSES as SENSOR_CLASSES, STATE_CLASSES as SENSOSR_STATE_CLASSES, ) -from homeassistant.components.zone.const import DOMAIN as ZONE_DOMAIN +from homeassistant.components.zone import DOMAIN as ZONE_DOMAIN from homeassistant.const import ( ATTR_DEVICE_ID, ATTR_DOMAIN, diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index 17a4acc1742..2e855c7af8d 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -50,11 +50,12 @@ from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType -from .const import ( +from .const import ( # noqa: F401 CALL_TYPE_COIL, CALL_TYPE_DISCRETE, CALL_TYPE_REGISTER_HOLDING, CALL_TYPE_REGISTER_INPUT, + CALL_TYPE_WRITE_REGISTER, CALL_TYPE_X_COILS, CALL_TYPE_X_REGISTER_HOLDINGS, CONF_BAUDRATE, @@ -63,6 +64,7 @@ from .const import ( CONF_CLOSE_COMM_ON_ERROR, CONF_DATA_TYPE, CONF_FANS, + CONF_HUB, CONF_INPUT_TYPE, CONF_LAZY_ERROR, CONF_MAX_TEMP, diff --git a/homeassistant/components/modbus/climate.py b/homeassistant/components/modbus/climate.py index eb2706c6334..7d6376a5a42 100644 --- a/homeassistant/components/modbus/climate.py +++ b/homeassistant/components/modbus/climate.py @@ -5,8 +5,11 @@ from datetime import datetime import struct from typing import Any -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ClimateEntityFeature, HVACMode +from homeassistant.components.climate import ( + ClimateEntity, + ClimateEntityFeature, + HVACMode, +) from homeassistant.const import ( ATTR_TEMPERATURE, CONF_NAME, diff --git a/homeassistant/components/modern_forms/translations/cs.json b/homeassistant/components/modern_forms/translations/cs.json new file mode 100644 index 00000000000..4ccfa17e6d3 --- /dev/null +++ b/homeassistant/components/modern_forms/translations/cs.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" + }, + "step": { + "user": { + "data": { + "host": "Hostitel" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/moehlenhoff_alpha2/climate.py b/homeassistant/components/moehlenhoff_alpha2/climate.py index e14f4801661..5eac2095706 100644 --- a/homeassistant/components/moehlenhoff_alpha2/climate.py +++ b/homeassistant/components/moehlenhoff_alpha2/climate.py @@ -1,8 +1,9 @@ """Support for Alpha2 room control unit via Alpha2 base.""" import logging +from typing import Any -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( + ClimateEntity, ClimateEntityFeature, HVACAction, HVACMode, @@ -110,7 +111,7 @@ class Alpha2Climate(CoordinatorEntity[Alpha2BaseCoordinator], ClimateEntity): self.coordinator.data["heat_areas"][self.heat_area_id].get("T_TARGET", 0.0) ) - async def async_set_temperature(self, **kwargs) -> None: + async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperatures.""" if (target_temperature := kwargs.get(ATTR_TEMPERATURE)) is None: return diff --git a/homeassistant/components/mold_indicator/sensor.py b/homeassistant/components/mold_indicator/sensor.py index db4f8d069ab..5685e76fac0 100644 --- a/homeassistant/components/mold_indicator/sensor.py +++ b/homeassistant/components/mold_indicator/sensor.py @@ -22,6 +22,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.typing import ConfigType, DiscoveryInfoType +from homeassistant.util.unit_conversion import TemperatureConverter _LOGGER = logging.getLogger(__name__) @@ -112,7 +113,7 @@ class MoldIndicator(SensorEntity): self._indoor_hum = None self._crit_temp = None - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register callbacks.""" @callback @@ -218,7 +219,7 @@ class MoldIndicator(SensorEntity): # convert to celsius if necessary if unit == TEMP_FAHRENHEIT: - return util.temperature.fahrenheit_to_celsius(temp) + return TemperatureConverter.convert(temp, TEMP_FAHRENHEIT, TEMP_CELSIUS) if unit == TEMP_CELSIUS: return temp _LOGGER.error( @@ -273,7 +274,7 @@ class MoldIndicator(SensorEntity): return hum - async def async_update(self): + async def async_update(self) -> None: """Calculate latest state.""" _LOGGER.debug("Update state for %s", self.entity_id) # check all sensors @@ -385,13 +386,13 @@ class MoldIndicator(SensorEntity): } dewpoint = ( - util.temperature.celsius_to_fahrenheit(self._dewpoint) + TemperatureConverter.convert(self._dewpoint, TEMP_CELSIUS, TEMP_FAHRENHEIT) if self._dewpoint is not None else None ) crit_temp = ( - util.temperature.celsius_to_fahrenheit(self._crit_temp) + TemperatureConverter.convert(self._crit_temp, TEMP_CELSIUS, TEMP_FAHRENHEIT) if self._crit_temp is not None else None ) diff --git a/homeassistant/components/monoprice/media_player.py b/homeassistant/components/monoprice/media_player.py index 627d95427e2..2f4a4a33f49 100644 --- a/homeassistant/components/monoprice/media_player.py +++ b/homeassistant/components/monoprice/media_player.py @@ -7,9 +7,10 @@ from homeassistant import core from homeassistant.components.media_player import ( MediaPlayerEntity, MediaPlayerEntityFeature, + MediaPlayerState, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_PORT, STATE_OFF, STATE_ON +from homeassistant.const import CONF_PORT from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv, entity_platform, service from homeassistant.helpers.entity import DeviceInfo @@ -142,7 +143,7 @@ class MonopriceZone(MediaPlayerEntity): self._mute = None self._update_success = True - def update(self): + def update(self) -> None: """Retrieve latest state.""" try: state = self._monoprice.zone_status(self._zone_id) @@ -155,7 +156,7 @@ class MonopriceZone(MediaPlayerEntity): self._update_success = False return - self._state = STATE_ON if state.power else STATE_OFF + self._state = MediaPlayerState.ON if state.power else MediaPlayerState.OFF self._volume = state.volume self._mute = state.mute idx = state.source @@ -165,7 +166,7 @@ class MonopriceZone(MediaPlayerEntity): self._source = None @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._zone_id < 20 or self._update_success @@ -231,36 +232,36 @@ class MonopriceZone(MediaPlayerEntity): self._monoprice.restore_zone(self._snapshot) self.schedule_update_ha_state(True) - def select_source(self, source): + def select_source(self, source: str) -> None: """Set input source.""" if source not in self._source_name_id: return idx = self._source_name_id[source] self._monoprice.set_source(self._zone_id, idx) - def turn_on(self): + def turn_on(self) -> None: """Turn the media player on.""" self._monoprice.set_power(self._zone_id, True) - def turn_off(self): + def turn_off(self) -> None: """Turn the media player off.""" self._monoprice.set_power(self._zone_id, False) - def mute_volume(self, mute): + def mute_volume(self, mute: bool) -> None: """Mute (true) or unmute (false) media player.""" self._monoprice.set_mute(self._zone_id, mute) - def set_volume_level(self, volume): + def set_volume_level(self, volume: float) -> None: """Set volume level, range 0..1.""" self._monoprice.set_volume(self._zone_id, int(volume * 38)) - def volume_up(self): + def volume_up(self) -> None: """Volume up the media player.""" if self._volume is None: return self._monoprice.set_volume(self._zone_id, min(self._volume + 1, 38)) - def volume_down(self): + def volume_down(self) -> None: """Volume down media player.""" if self._volume is None: return diff --git a/homeassistant/components/moon/sensor.py b/homeassistant/components/moon/sensor.py index f3a3b3f48fa..b033dccc296 100644 --- a/homeassistant/components/moon/sensor.py +++ b/homeassistant/components/moon/sensor.py @@ -15,6 +15,7 @@ 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 @@ -52,6 +53,15 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the Moon sensor.""" + 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, diff --git a/homeassistant/components/moon/strings.json b/homeassistant/components/moon/strings.json index d5bb204a740..76ce886ded8 100644 --- a/homeassistant/components/moon/strings.json +++ b/homeassistant/components/moon/strings.json @@ -9,5 +9,11 @@ "abort": { "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" } + }, + "issues": { + "removed_yaml": { + "title": "The Moon YAML configuration has been removed", + "description": "Configuring Moon using YAML has been removed.\n\nYour existing YAML configuration is not used by Home Assistant.\n\nRemove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } } } diff --git a/homeassistant/components/moon/translations/de.json b/homeassistant/components/moon/translations/de.json index 4d8d2d45284..00408fde1b0 100644 --- a/homeassistant/components/moon/translations/de.json +++ b/homeassistant/components/moon/translations/de.json @@ -9,5 +9,11 @@ } } }, + "issues": { + "removed_yaml": { + "description": "Das Konfigurieren von Mond mit YAML wurde entfernt. \n\nDeine vorhandene YAML-Konfiguration wird von Home Assistant nicht verwendet. \n\nEntferne die YAML-Konfiguration aus deiner configuration.yaml-Datei und starte Home Assistant neu, um dieses Problem zu beheben.", + "title": "Die Mond YAML-Konfiguration wurde entfernt" + } + }, "title": "Mond" } \ No newline at end of file diff --git a/homeassistant/components/moon/translations/en.json b/homeassistant/components/moon/translations/en.json index 0f324f7b64b..2f6f73a9982 100644 --- a/homeassistant/components/moon/translations/en.json +++ b/homeassistant/components/moon/translations/en.json @@ -9,5 +9,11 @@ } } }, + "issues": { + "removed_yaml": { + "description": "Configuring Moon using YAML has been removed.\n\nYour existing YAML configuration is not used by Home Assistant.\n\nRemove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "title": "The Moon YAML configuration has been removed" + } + }, "title": "Moon" } \ No newline at end of file diff --git a/homeassistant/components/moon/translations/es.json b/homeassistant/components/moon/translations/es.json index 4bd878ba9f3..2cdf14282e2 100644 --- a/homeassistant/components/moon/translations/es.json +++ b/homeassistant/components/moon/translations/es.json @@ -9,5 +9,11 @@ } } }, + "issues": { + "removed_yaml": { + "description": "Se ha eliminado la configuraci\u00f3n de Moon mediante YAML. \n\nHome Assistant no utiliza tu configuraci\u00f3n YAML existente. \n\nElimina la configuraci\u00f3n YAML de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "title": "Se ha eliminado la configuraci\u00f3n YAML de Moon" + } + }, "title": "Luna" } \ No newline at end of file diff --git a/homeassistant/components/moon/translations/ru.json b/homeassistant/components/moon/translations/ru.json index 90f93873205..cc9beaddb52 100644 --- a/homeassistant/components/moon/translations/ru.json +++ b/homeassistant/components/moon/translations/ru.json @@ -9,5 +9,11 @@ } } }, + "issues": { + "removed_yaml": { + "description": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0441\u0435\u043d\u0441\u043e\u0440\u0430 \u041b\u0443\u043d\u044b \u0442\u0435\u043f\u0435\u0440\u044c \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u0430 \u0442\u043e\u043b\u044c\u043a\u043e \u0447\u0435\u0440\u0435\u0437 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0439 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f YAML-\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043d\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f Home Assistant. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0435\u0451 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "title": "\u0423\u0434\u0430\u043b\u0435\u043d\u0430 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0441\u0435\u043d\u0441\u043e\u0440\u0430 \u041b\u0443\u043d\u044b \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML" + } + }, "title": "\u041b\u0443\u043d\u0430" } \ No newline at end of file diff --git a/homeassistant/components/moon/translations/zh-Hant.json b/homeassistant/components/moon/translations/zh-Hant.json index c84da0f79f2..29acac079e1 100644 --- a/homeassistant/components/moon/translations/zh-Hant.json +++ b/homeassistant/components/moon/translations/zh-Hant.json @@ -9,5 +9,11 @@ } } }, + "issues": { + "removed_yaml": { + "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a Moon \u7684\u529f\u80fd\u5373\u5c07\u79fb\u9664\u3002\n\nHome Assistant \u5c07\u4e0d\u518d\u4f7f\u7528\u73fe\u6709\u7684 YAML \u8a2d\u5b9a\u3002\n\n\u7531 configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u555f Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "Moon YAML \u8a2d\u5b9a\u5373\u5c07\u79fb\u9664" + } + }, "title": "\u6708\u76f8" } \ No newline at end of file diff --git a/homeassistant/components/motion_blinds/sensor.py b/homeassistant/components/motion_blinds/sensor.py index 3a6f775092e..04b7a91fe31 100644 --- a/homeassistant/components/motion_blinds/sensor.py +++ b/homeassistant/components/motion_blinds/sensor.py @@ -61,7 +61,7 @@ class MotionBatterySensor(CoordinatorEntity, SensorEntity): self._attr_unique_id = f"{blind.mac}-battery" @property - def available(self): + def available(self) -> bool: """Return True if entity is available.""" if self.coordinator.data is None: return False @@ -81,12 +81,12 @@ class MotionBatterySensor(CoordinatorEntity, SensorEntity): """Return device specific state attributes.""" return {ATTR_BATTERY_VOLTAGE: self._blind.battery_voltage} - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Subscribe to multicast pushes.""" self._blind.Register_callback(self.unique_id, self.schedule_update_ha_state) await super().async_added_to_hass() - async def async_will_remove_from_hass(self): + async def async_will_remove_from_hass(self) -> None: """Unsubscribe when removed.""" self._blind.Remove_callback(self.unique_id) await super().async_will_remove_from_hass() @@ -145,7 +145,7 @@ class MotionSignalStrengthSensor(CoordinatorEntity, SensorEntity): self._attr_name = name @property - def available(self): + def available(self) -> bool: """Return True if entity is available.""" if self.coordinator.data is None: return False @@ -164,12 +164,12 @@ class MotionSignalStrengthSensor(CoordinatorEntity, SensorEntity): """Return the state of the sensor.""" return self._device.RSSI - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Subscribe to multicast pushes.""" self._device.Register_callback(self.unique_id, self.schedule_update_ha_state) await super().async_added_to_hass() - async def async_will_remove_from_hass(self): + async def async_will_remove_from_hass(self) -> None: """Unsubscribe when removed.""" self._device.Remove_callback(self.unique_id) await super().async_will_remove_from_hass() diff --git a/homeassistant/components/motioneye/__init__.py b/homeassistant/components/motioneye/__init__.py index 6a650142995..c7aa8edc6c9 100644 --- a/homeassistant/components/motioneye/__init__.py +++ b/homeassistant/components/motioneye/__init__.py @@ -35,8 +35,8 @@ from motioneye_client.const import ( KEY_WEB_HOOK_STORAGE_URL, ) -from homeassistant.components.camera.const import DOMAIN as CAMERA_DOMAIN -from homeassistant.components.media_source.const import URI_SCHEME +from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN +from homeassistant.components.media_source import URI_SCHEME from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.components.webhook import ( diff --git a/homeassistant/components/motioneye/media_source.py b/homeassistant/components/motioneye/media_source.py index 915cc30897e..85fe3985b93 100644 --- a/homeassistant/components/motioneye/media_source.py +++ b/homeassistant/components/motioneye/media_source.py @@ -7,13 +7,7 @@ from typing import Optional, cast from motioneye_client.const import KEY_MEDIA_LIST, KEY_MIME_TYPE, KEY_PATH -from homeassistant.components.media_player.const import ( - MEDIA_CLASS_DIRECTORY, - MEDIA_CLASS_IMAGE, - MEDIA_CLASS_VIDEO, - MEDIA_TYPE_IMAGE, - MEDIA_TYPE_VIDEO, -) +from homeassistant.components.media_player import MediaClass, MediaType from homeassistant.components.media_source.error import MediaSourceError, Unresolvable from homeassistant.components.media_source.models import ( BrowseMediaSource, @@ -34,8 +28,8 @@ MIME_TYPE_MAP = { } MEDIA_CLASS_MAP = { - "movies": MEDIA_CLASS_VIDEO, - "images": MEDIA_CLASS_IMAGE, + "movies": MediaClass.VIDEO, + "images": MediaClass.IMAGE, } _LOGGER = logging.getLogger(__name__) @@ -172,12 +166,12 @@ class MotionEyeMediaSource(MediaSource): return BrowseMediaSource( domain=DOMAIN, identifier=config.entry_id, - media_class=MEDIA_CLASS_DIRECTORY, + media_class=MediaClass.DIRECTORY, media_content_type="", title=config.title, can_play=False, can_expand=True, - children_media_class=MEDIA_CLASS_DIRECTORY, + children_media_class=MediaClass.DIRECTORY, ) def _build_media_configs(self) -> BrowseMediaSource: @@ -185,7 +179,7 @@ class MotionEyeMediaSource(MediaSource): return BrowseMediaSource( domain=DOMAIN, identifier="", - media_class=MEDIA_CLASS_DIRECTORY, + media_class=MediaClass.DIRECTORY, media_content_type="", title="motionEye Media", can_play=False, @@ -194,7 +188,7 @@ class MotionEyeMediaSource(MediaSource): self._build_media_config(entry) for entry in self.hass.config_entries.async_entries(DOMAIN) ], - children_media_class=MEDIA_CLASS_DIRECTORY, + children_media_class=MediaClass.DIRECTORY, ) @classmethod @@ -207,12 +201,12 @@ class MotionEyeMediaSource(MediaSource): return BrowseMediaSource( domain=DOMAIN, identifier=f"{config.entry_id}#{device.id}", - media_class=MEDIA_CLASS_DIRECTORY, + media_class=MediaClass.DIRECTORY, media_content_type="", title=f"{config.title} {device.name}" if full_title else device.name, can_play=False, can_expand=True, - children_media_class=MEDIA_CLASS_DIRECTORY, + children_media_class=MediaClass.DIRECTORY, ) def _build_media_devices(self, config: ConfigEntry) -> BrowseMediaSource: @@ -238,9 +232,9 @@ class MotionEyeMediaSource(MediaSource): return BrowseMediaSource( domain=DOMAIN, identifier=f"{config.entry_id}#{device.id}#{kind}", - media_class=MEDIA_CLASS_DIRECTORY, + media_class=MediaClass.DIRECTORY, media_content_type=( - MEDIA_TYPE_VIDEO if kind == "movies" else MEDIA_TYPE_IMAGE + MediaType.VIDEO if kind == "movies" else MediaType.IMAGE ), title=( f"{config.title} {device.name} {kind.title()}" @@ -250,7 +244,7 @@ class MotionEyeMediaSource(MediaSource): can_play=False, can_expand=True, children_media_class=( - MEDIA_CLASS_VIDEO if kind == "movies" else MEDIA_CLASS_IMAGE + MediaClass.VIDEO if kind == "movies" else MediaClass.IMAGE ), ) @@ -340,16 +334,16 @@ class MotionEyeMediaSource(MediaSource): f"{config.entry_id}#{device.id}" f"#{kind}#{full_child_path}" ), - media_class=MEDIA_CLASS_DIRECTORY, + media_class=MediaClass.DIRECTORY, media_content_type=( - MEDIA_TYPE_VIDEO + MediaType.VIDEO if kind == "movies" - else MEDIA_TYPE_IMAGE + else MediaType.IMAGE ), title=display_child_path, can_play=False, can_expand=True, - children_media_class=MEDIA_CLASS_DIRECTORY, + children_media_class=MediaClass.DIRECTORY, ) ) return base diff --git a/homeassistant/components/motioneye/translations/cs.json b/homeassistant/components/motioneye/translations/cs.json index 311a1d4d965..45f178ef29e 100644 --- a/homeassistant/components/motioneye/translations/cs.json +++ b/homeassistant/components/motioneye/translations/cs.json @@ -13,6 +13,10 @@ "step": { "user": { "data": { + "admin_password": "Heslo spr\u00e1vce", + "admin_username": "U\u017eivatelsk\u00e9 jm\u00e9no spr\u00e1vce", + "surveillance_password": "Heslo pro dohled", + "surveillance_username": "U\u017eivatelsk\u00e9 jm\u00e9no pro dohled", "url": "URL" } } diff --git a/homeassistant/components/motioneye/translations/es.json b/homeassistant/components/motioneye/translations/es.json index d7dc7f1a083..5db1a3f8869 100644 --- a/homeassistant/components/motioneye/translations/es.json +++ b/homeassistant/components/motioneye/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "El servicio ya est\u00e1 configurado", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "error": { "cannot_connect": "No se pudo conectar", diff --git a/homeassistant/components/mpd/media_player.py b/homeassistant/components/mpd/media_player.py index ecee057a653..44d94bc649f 100644 --- a/homeassistant/components/mpd/media_player.py +++ b/homeassistant/components/mpd/media_player.py @@ -6,6 +6,7 @@ from datetime import timedelta import hashlib import logging import os +from typing import Any import mpd from mpd.asyncio import MPDClient @@ -14,28 +15,15 @@ import voluptuous as vol from homeassistant.components import media_source from homeassistant.components.media_player import ( PLATFORM_SCHEMA, + BrowseMedia, MediaPlayerEntity, MediaPlayerEntityFeature, -) -from homeassistant.components.media_player.browse_media import ( + MediaPlayerState, + MediaType, + RepeatMode, async_process_play_media_url, ) -from homeassistant.components.media_player.const import ( - MEDIA_TYPE_MUSIC, - MEDIA_TYPE_PLAYLIST, - REPEAT_MODE_ALL, - REPEAT_MODE_OFF, - REPEAT_MODE_ONE, -) -from homeassistant.const import ( - CONF_HOST, - CONF_NAME, - CONF_PASSWORD, - CONF_PORT, - STATE_OFF, - STATE_PAUSED, - STATE_PLAYING, -) +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_PORT from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -95,6 +83,8 @@ async def async_setup_platform( class MpdDevice(MediaPlayerEntity): """Representation of a MPD server.""" + _attr_media_content_type = MediaType.MUSIC + # pylint: disable=no-member def __init__(self, server, port, password, name): """Initialize the MPD device.""" @@ -164,7 +154,7 @@ class MpdDevice(MediaPlayerEntity): """Return true if MPD is available and connected.""" return self._is_connected - async def async_update(self): + async def async_update(self) -> None: """Get the latest data and update the state.""" try: if not self._is_connected: @@ -183,18 +173,18 @@ class MpdDevice(MediaPlayerEntity): return self._name @property - def state(self): + def state(self) -> MediaPlayerState: """Return the media state.""" if self._status is None: - return STATE_OFF + return MediaPlayerState.OFF if self._status["state"] == "play": - return STATE_PLAYING + return MediaPlayerState.PLAYING if self._status["state"] == "pause": - return STATE_PAUSED + return MediaPlayerState.PAUSED if self._status["state"] == "stop": - return STATE_OFF + return MediaPlayerState.OFF - return STATE_OFF + return MediaPlayerState.OFF @property def is_volume_muted(self): @@ -206,11 +196,6 @@ class MpdDevice(MediaPlayerEntity): """Return the content ID of current playing media.""" return self._currentsong.get("file") - @property - def media_content_type(self): - """Return the content type of current playing media.""" - return MEDIA_TYPE_MUSIC - @property def media_duration(self): """Return the duration of current playing media in seconds.""" @@ -273,7 +258,7 @@ class MpdDevice(MediaPlayerEntity): """Hash value for media image.""" return self._media_image_hash - async def async_get_media_image(self): + async def async_get_media_image(self) -> tuple[bytes | None, str | None]: """Fetch media image of current playing track.""" if not (file := self._currentsong.get("file")): return None, None @@ -380,12 +365,12 @@ class MpdDevice(MediaPlayerEntity): """Return the list of available input sources.""" return self._playlists - async def async_select_source(self, source): + async def async_select_source(self, source: str) -> None: """Choose a different available playlist and play it.""" - await self.async_play_media(MEDIA_TYPE_PLAYLIST, source) + await self.async_play_media(MediaType.PLAYLIST, source) @Throttle(PLAYLIST_UPDATE_INTERVAL) - async def _update_playlists(self, **kwargs): + async def _update_playlists(self, **kwargs: Any) -> None: """Update available MPD playlists.""" try: self._playlists = [] @@ -395,12 +380,12 @@ class MpdDevice(MediaPlayerEntity): self._playlists = None _LOGGER.warning("Playlists could not be updated: %s:", error) - async def async_set_volume_level(self, volume): + async def async_set_volume_level(self, volume: float) -> None: """Set volume of media player.""" if "volume" in self._status: await self._client.setvol(int(volume * 100)) - async def async_volume_up(self): + async def async_volume_up(self) -> None: """Service to send the MPD the command for volume up.""" if "volume" in self._status: current_volume = int(self._status["volume"]) @@ -408,7 +393,7 @@ class MpdDevice(MediaPlayerEntity): if current_volume <= 100: self._client.setvol(current_volume + 5) - async def async_volume_down(self): + async def async_volume_down(self) -> None: """Service to send the MPD the command for volume down.""" if "volume" in self._status: current_volume = int(self._status["volume"]) @@ -416,30 +401,30 @@ class MpdDevice(MediaPlayerEntity): if current_volume >= 0: await self._client.setvol(current_volume - 5) - async def async_media_play(self): + async def async_media_play(self) -> None: """Service to send the MPD the command for play/pause.""" if self._status["state"] == "pause": await self._client.pause(0) else: await self._client.play() - async def async_media_pause(self): + async def async_media_pause(self) -> None: """Service to send the MPD the command for play/pause.""" await self._client.pause(1) - async def async_media_stop(self): + async def async_media_stop(self) -> None: """Service to send the MPD the command for stop.""" await self._client.stop() - async def async_media_next_track(self): + async def async_media_next_track(self) -> None: """Service to send the MPD the command for next track.""" await self._client.next() - async def async_media_previous_track(self): + async def async_media_previous_track(self) -> None: """Service to send the MPD the command for previous track.""" await self._client.previous() - async def async_mute_volume(self, mute): + async def async_mute_volume(self, mute: bool) -> None: """Mute. Emulated with set_volume_level.""" if "volume" in self._status: if mute: @@ -449,16 +434,18 @@ class MpdDevice(MediaPlayerEntity): await self.async_set_volume_level(self._muted_volume) self._muted = mute - async def async_play_media(self, media_type, media_id, **kwargs): + async def async_play_media( + self, media_type: str, media_id: str, **kwargs: Any + ) -> None: """Send the media player the command for playing a playlist.""" if media_source.is_media_source_id(media_id): - media_type = MEDIA_TYPE_MUSIC + media_type = MediaType.MUSIC play_item = await media_source.async_resolve_media( self.hass, media_id, self.entity_id ) media_id = async_process_play_media_url(self.hass, play_item.url) - if media_type == MEDIA_TYPE_PLAYLIST: + if media_type == MediaType.PLAYLIST: _LOGGER.debug("Playing playlist: %s", media_id) if media_id in self._playlists: self._currentplaylist = media_id @@ -475,22 +462,22 @@ class MpdDevice(MediaPlayerEntity): await self._client.play() @property - def repeat(self): + def repeat(self) -> RepeatMode: """Return current repeat mode.""" if self._status["repeat"] == "1": if self._status["single"] == "1": - return REPEAT_MODE_ONE - return REPEAT_MODE_ALL - return REPEAT_MODE_OFF + return RepeatMode.ONE + return RepeatMode.ALL + return RepeatMode.OFF - async def async_set_repeat(self, repeat): + async def async_set_repeat(self, repeat: RepeatMode) -> None: """Set repeat mode.""" - if repeat == REPEAT_MODE_OFF: + if repeat == RepeatMode.OFF: await self._client.repeat(0) await self._client.single(0) else: await self._client.repeat(1) - if repeat == REPEAT_MODE_ONE: + if repeat == RepeatMode.ONE: await self._client.single(1) else: await self._client.single(0) @@ -500,28 +487,30 @@ class MpdDevice(MediaPlayerEntity): """Boolean if shuffle is enabled.""" return bool(int(self._status["random"])) - async def async_set_shuffle(self, shuffle): + async def async_set_shuffle(self, shuffle: bool) -> None: """Enable/disable shuffle mode.""" await self._client.random(int(shuffle)) - async def async_turn_off(self): + async def async_turn_off(self) -> None: """Service to send the MPD the command to stop playing.""" await self._client.stop() - async def async_turn_on(self): + async def async_turn_on(self) -> None: """Service to send the MPD the command to start playing.""" await self._client.play() await self._update_playlists(no_throttle=True) - async def async_clear_playlist(self): + async def async_clear_playlist(self) -> None: """Clear players playlist.""" await self._client.clear() - async def async_media_seek(self, position): + async def async_media_seek(self, position: float) -> None: """Send seek command.""" await self._client.seekcur(position) - 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.""" return await media_source.async_browse_media( self.hass, diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index c14266e296f..62aad6ca7fe 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -76,7 +76,6 @@ from .const import ( # noqa: F401 PLATFORMS, RELOADABLE_PLATFORMS, ) -from .mixins import MqttData from .models import ( # noqa: F401 MqttCommandTemplate, MqttValueTemplate, @@ -86,6 +85,7 @@ from .models import ( # noqa: F401 ) from .util import ( _VALID_QOS_SCHEMA, + get_mqtt_data, mqtt_config_entry_enabled, valid_publish_topic, valid_subscribe_topic, @@ -164,13 +164,12 @@ async def _async_setup_discovery( async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Start the MQTT protocol service.""" - mqtt_data: MqttData = hass.data.setdefault(DATA_MQTT, MqttData()) + mqtt_data = get_mqtt_data(hass, True) conf: ConfigType | None = config.get(DOMAIN) websocket_api.async_register_command(hass, websocket_subscribe) websocket_api.async_register_command(hass, websocket_mqtt_info) - debug_info.initialize(hass) if conf: conf = dict(conf) @@ -249,25 +248,12 @@ async def _async_config_entry_updated(hass: HomeAssistant, entry: ConfigEntry) - Causes for this is config entry options changing. """ - mqtt_data: MqttData = hass.data[DATA_MQTT] - assert (client := mqtt_data.client) is not None - - if (conf := mqtt_data.config) is None: - conf = CONFIG_SCHEMA_BASE(dict(entry.data)) - - mqtt_data.config = _merge_extended_config(entry, conf) - await client.async_disconnect() - client.init_client() - await client.async_connect() - - await discovery.async_stop(hass) - if client.conf.get(CONF_DISCOVERY): - await _async_setup_discovery(hass, cast(ConfigType, mqtt_data.config), entry) + await hass.config_entries.async_reload(entry.entry_id) async def async_fetch_config(hass: HomeAssistant, entry: ConfigEntry) -> dict | None: """Fetch fresh MQTT yaml config from the hass config when (re)loading the entry.""" - mqtt_data: MqttData = hass.data[DATA_MQTT] + mqtt_data = get_mqtt_data(hass) if mqtt_data.reload_entry: hass_config = await conf_util.async_hass_config_yaml(hass) mqtt_data.config = CONFIG_SCHEMA_BASE(hass_config.get(DOMAIN, {})) @@ -307,7 +293,7 @@ async def async_fetch_config(hass: HomeAssistant, entry: ConfigEntry) -> dict | async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Load a config entry.""" - mqtt_data: MqttData = hass.data.setdefault(DATA_MQTT, MqttData()) + mqtt_data = get_mqtt_data(hass, True) # Merge basic configuration, and add missing defaults for basic options if (conf := await async_fetch_config(hass, entry)) is None: @@ -318,7 +304,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if mqtt_data.subscriptions_to_restore: mqtt_data.client.subscriptions = mqtt_data.subscriptions_to_restore mqtt_data.subscriptions_to_restore = [] - entry.add_update_listener(_async_config_entry_updated) + mqtt_data.reload_dispatchers.append( + entry.add_update_listener(_async_config_entry_updated) + ) await mqtt_data.client.async_connect() @@ -593,7 +581,7 @@ def async_subscribe_connection_status( def is_connected(hass: HomeAssistant) -> bool: """Return if MQTT client is connected.""" - mqtt_data: MqttData = hass.data[DATA_MQTT] + mqtt_data = get_mqtt_data(hass) assert mqtt_data.client is not None return mqtt_data.client.connected @@ -611,7 +599,7 @@ async def async_remove_config_entry_device( async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload MQTT dump and publish service when the config entry is unloaded.""" - mqtt_data: MqttData = hass.data[DATA_MQTT] + mqtt_data = get_mqtt_data(hass) assert mqtt_data.client is not None mqtt_client = mqtt_data.client diff --git a/homeassistant/components/mqtt/binary_sensor.py b/homeassistant/components/mqtt/binary_sensor.py index 32608e86f53..f0e5ecc9df8 100644 --- a/homeassistant/components/mqtt/binary_sensor.py +++ b/homeassistant/components/mqtt/binary_sensor.py @@ -180,6 +180,7 @@ class MqttBinarySensor(MqttEntity, BinarySensorEntity, RestoreEntity): return DISCOVERY_SCHEMA def _setup_from_config(self, config): + self._attr_force_update = config[CONF_FORCE_UPDATE] self._value_template = MqttValueTemplate( self._config.get(CONF_VALUE_TEMPLATE), entity=self, @@ -296,11 +297,6 @@ class MqttBinarySensor(MqttEntity, BinarySensorEntity, RestoreEntity): """Return the class of this sensor.""" return self._config.get(CONF_DEVICE_CLASS) - @property - def force_update(self) -> bool: - """Force update.""" - return self._config[CONF_FORCE_UPDATE] - @property def available(self) -> bool: """Return true if the device is available and value has not expired.""" diff --git a/homeassistant/components/mqtt/client.py b/homeassistant/components/mqtt/client.py index 50674706624..bd734318938 100644 --- a/homeassistant/components/mqtt/client.py +++ b/homeassistant/components/mqtt/client.py @@ -27,7 +27,14 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP, ) -from homeassistant.core import CoreState, Event, HassJob, HomeAssistant, callback +from homeassistant.core import ( + CALLBACK_TYPE, + CoreState, + Event, + HassJob, + HomeAssistant, + callback, +) from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.dispatcher import dispatcher_send from homeassistant.helpers.typing import ConfigType @@ -46,7 +53,6 @@ from .const import ( CONF_KEEPALIVE, CONF_TLS_INSECURE, CONF_WILL_MESSAGE, - DATA_MQTT, DEFAULT_ENCODING, DEFAULT_QOS, MQTT_CONNECTED, @@ -61,15 +67,13 @@ from .models import ( ReceiveMessage, ReceivePayloadType, ) -from .util import mqtt_config_entry_enabled +from .util import get_mqtt_data, mqtt_config_entry_enabled if TYPE_CHECKING: # Only import for paho-mqtt type checking here, imports are done locally # because integrations should be able to optionally rely on MQTT. import paho.mqtt.client as mqtt - from .mixins import MqttData - _LOGGER = logging.getLogger(__name__) @@ -100,11 +104,7 @@ async def async_publish( encoding: str | None = DEFAULT_ENCODING, ) -> None: """Publish message to a MQTT topic.""" - # Local import to avoid circular dependencies - # pylint: disable-next=import-outside-toplevel - from .mixins import MqttData - - mqtt_data: MqttData = hass.data.setdefault(DATA_MQTT, MqttData()) + mqtt_data = get_mqtt_data(hass, True) if mqtt_data.client is None or not mqtt_config_entry_enabled(hass): raise HomeAssistantError( f"Cannot publish to topic '{topic}', MQTT is not enabled" @@ -185,16 +185,12 @@ async def async_subscribe( | AsyncDeprecatedMessageCallbackType, qos: int = DEFAULT_QOS, encoding: str | None = DEFAULT_ENCODING, -): +) -> CALLBACK_TYPE: """Subscribe to an MQTT topic. Call the return value to unsubscribe. """ - # Local import to avoid circular dependencies - # pylint: disable-next=import-outside-toplevel - from .mixins import MqttData - - mqtt_data: MqttData = hass.data.setdefault(DATA_MQTT, MqttData()) + mqtt_data = get_mqtt_data(hass, True) if mqtt_data.client is None or not mqtt_config_entry_enabled(hass): raise HomeAssistantError( f"Cannot subscribe to topic '{topic}', MQTT is not enabled" @@ -332,7 +328,7 @@ class MQTT: # should be able to optionally rely on MQTT. import paho.mqtt.client as mqtt # pylint: disable=import-outside-toplevel - self._mqtt_data: MqttData = hass.data[DATA_MQTT] + self._mqtt_data = get_mqtt_data(hass) self.hass = hass self.config_entry = config_entry @@ -368,12 +364,12 @@ class MQTT: hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_stop_mqtt) ) - def cleanup(self): + def cleanup(self) -> None: """Clean up listeners.""" while self._cleanup_on_unload: self._cleanup_on_unload.pop()() - def init_client(self): + def init_client(self) -> None: """Initialize paho client.""" self._mqttc = MqttClientSetup(self.conf).client self._mqttc.on_connect = self._mqtt_on_connect @@ -408,7 +404,8 @@ class MQTT: self._mqttc.publish, topic, payload, qos, retain ) _LOGGER.debug( - "Transmitting message on %s: '%s', mid: %s", + "Transmitting%s message on %s: '%s', mid: %s", + " retained" if retain else "", topic, payload, msg_info.mid, @@ -439,10 +436,10 @@ class MQTT: self._mqttc.loop_start() - async def async_disconnect(self): + async def async_disconnect(self) -> None: """Stop the MQTT client.""" - def stop(): + def stop() -> None: """Stop the MQTT client.""" # Do not disconnect, we want the broker to always publish will self._mqttc.loop_stop() @@ -626,9 +623,9 @@ class MQTT: @callback def _mqtt_handle_message(self, msg: MQTTMessage) -> None: _LOGGER.debug( - "Received message on %s%s: %s", + "Received%s message on %s: %s", + " retained" if msg.retain else "", msg.topic, - " (retained)" if msg.retain else "", msg.payload[0:8192], ) timestamp = dt_util.utcnow() diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index 7226d1f8d1f..96c7ca3665b 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -8,8 +8,7 @@ from typing import Any import voluptuous as vol from homeassistant.components import climate -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( ATTR_HVAC_MODE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, @@ -22,6 +21,7 @@ from homeassistant.components.climate.const import ( PRESET_NONE, SWING_OFF, SWING_ON, + ClimateEntity, ClimateEntityFeature, HVACAction, HVACMode, diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py index 12d97b41a74..5d21619c498 100644 --- a/homeassistant/components/mqtt/config_flow.py +++ b/homeassistant/components/mqtt/config_flow.py @@ -18,8 +18,9 @@ from homeassistant.const import ( CONF_PROTOCOL, CONF_USERNAME, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.typing import ConfigType from .client import MqttClientSetup from .const import ( @@ -30,14 +31,12 @@ from .const import ( CONF_BIRTH_MESSAGE, CONF_BROKER, CONF_WILL_MESSAGE, - DATA_MQTT, DEFAULT_BIRTH, DEFAULT_DISCOVERY, DEFAULT_WILL, DOMAIN, ) -from .mixins import MqttData -from .util import MQTT_WILL_BIRTH_SCHEMA +from .util import MQTT_WILL_BIRTH_SCHEMA, get_mqtt_data MQTT_TIMEOUT = 5 @@ -75,7 +74,7 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if user_input is not None: can_connect = await self.hass.async_add_executor_job( try_connection, - self.hass, + get_mqtt_data(self.hass, True).config or {}, user_input[CONF_BROKER], user_input[CONF_PORT], user_input.get(CONF_USERNAME), @@ -119,7 +118,7 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): data = self._hassio_discovery can_connect = await self.hass.async_add_executor_job( try_connection, - self.hass, + get_mqtt_data(self.hass, True).config or {}, data[CONF_HOST], data[CONF_PORT], data.get(CONF_USERNAME), @@ -165,14 +164,14 @@ class MQTTOptionsFlowHandler(config_entries.OptionsFlow): self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Manage the MQTT broker configuration.""" - mqtt_data: MqttData = self.hass.data.setdefault(DATA_MQTT, MqttData()) + mqtt_data = get_mqtt_data(self.hass, True) + yaml_config = mqtt_data.config or {} errors = {} current_config = self.config_entry.data - yaml_config = mqtt_data.config or {} if user_input is not None: can_connect = await self.hass.async_add_executor_job( try_connection, - self.hass, + yaml_config, user_input[CONF_BROKER], user_input[CONF_PORT], user_input.get(CONF_USERNAME), @@ -216,7 +215,7 @@ class MQTTOptionsFlowHandler(config_entries.OptionsFlow): self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Manage the MQTT options.""" - mqtt_data: MqttData = self.hass.data.setdefault(DATA_MQTT, MqttData()) + mqtt_data = get_mqtt_data(self.hass, True) errors = {} current_config = self.config_entry.data yaml_config = mqtt_data.config or {} @@ -264,7 +263,9 @@ class MQTTOptionsFlowHandler(config_entries.OptionsFlow): updated_config.update(self.broker_config) updated_config.update(options_config) self.hass.config_entries.async_update_entry( - self.config_entry, data=updated_config + self.config_entry, + data=updated_config, + title=str(self.broker_config[CONF_BROKER]), ) return self.async_create_entry(title="", data={}) @@ -338,7 +339,7 @@ class MQTTOptionsFlowHandler(config_entries.OptionsFlow): def try_connection( - hass: HomeAssistant, + yaml_config: ConfigType, broker: str, port: int, username: str | None, @@ -351,8 +352,6 @@ def try_connection( import paho.mqtt.client as mqtt # pylint: disable=import-outside-toplevel # Get the config from configuration.yaml - mqtt_data: MqttData = hass.data.setdefault(DATA_MQTT, MqttData()) - yaml_config = mqtt_data.config or {} entry_config = { CONF_BROKER: broker, CONF_PORT: port, diff --git a/homeassistant/components/mqtt/debug_info.py b/homeassistant/components/mqtt/debug_info.py index 17dbc27f0c4..5fae98eaea5 100644 --- a/homeassistant/components/mqtt/debug_info.py +++ b/homeassistant/components/mqtt/debug_info.py @@ -11,29 +11,26 @@ import attr from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.typing import DiscoveryInfoType from homeassistant.util import dt as dt_util from .const import ATTR_DISCOVERY_PAYLOAD, ATTR_DISCOVERY_TOPIC from .models import MessageCallbackType, PublishPayloadType +from .util import get_mqtt_data -DATA_MQTT_DEBUG_INFO = "mqtt_debug_info" STORED_MESSAGES = 10 -def initialize(hass: HomeAssistant): - """Initialize MQTT debug info.""" - hass.data[DATA_MQTT_DEBUG_INFO] = {"entities": {}, "triggers": {}} - - def log_messages( hass: HomeAssistant, entity_id: str ) -> Callable[[MessageCallbackType], MessageCallbackType]: """Wrap an MQTT message callback to support message logging.""" + debug_info_entities = get_mqtt_data(hass).debug_info_entities + def _log_message(msg): """Log message.""" - debug_info = hass.data[DATA_MQTT_DEBUG_INFO] - messages = debug_info["entities"][entity_id]["subscriptions"][ + messages = debug_info_entities[entity_id]["subscriptions"][ msg.subscribed_topic ]["messages"] if msg not in messages: @@ -72,8 +69,7 @@ def log_message( retain: bool, ) -> None: """Log an outgoing MQTT message.""" - debug_info = hass.data[DATA_MQTT_DEBUG_INFO] - entity_info = debug_info["entities"].setdefault( + entity_info = get_mqtt_data(hass).debug_info_entities.setdefault( entity_id, {"subscriptions": {}, "discovery_data": {}, "transmitted": {}} ) if topic not in entity_info["transmitted"]: @@ -86,11 +82,14 @@ def log_message( entity_info["transmitted"][topic]["messages"].append(msg) -def add_subscription(hass, message_callback, subscription): +def add_subscription( + hass: HomeAssistant, + message_callback: MessageCallbackType, + subscription: str, +) -> None: """Prepare debug data for subscription.""" if entity_id := getattr(message_callback, "__entity_id", None): - debug_info = hass.data[DATA_MQTT_DEBUG_INFO] - entity_info = debug_info["entities"].setdefault( + entity_info = get_mqtt_data(hass).debug_info_entities.setdefault( entity_id, {"subscriptions": {}, "discovery_data": {}, "transmitted": {}} ) if subscription not in entity_info["subscriptions"]: @@ -101,65 +100,81 @@ def add_subscription(hass, message_callback, subscription): entity_info["subscriptions"][subscription]["count"] += 1 -def remove_subscription(hass, message_callback, subscription): +def remove_subscription( + hass: HomeAssistant, + message_callback: MessageCallbackType, + subscription: str, +) -> None: """Remove debug data for subscription if it exists.""" - entity_id = getattr(message_callback, "__entity_id", None) - if entity_id and entity_id in hass.data[DATA_MQTT_DEBUG_INFO]["entities"]: - hass.data[DATA_MQTT_DEBUG_INFO]["entities"][entity_id]["subscriptions"][ - subscription - ]["count"] -= 1 - if not hass.data[DATA_MQTT_DEBUG_INFO]["entities"][entity_id]["subscriptions"][ - subscription - ]["count"]: - hass.data[DATA_MQTT_DEBUG_INFO]["entities"][entity_id]["subscriptions"].pop( - subscription - ) + if (entity_id := getattr(message_callback, "__entity_id", None)) and entity_id in ( + debug_info_entities := get_mqtt_data(hass).debug_info_entities + ): + debug_info_entities[entity_id]["subscriptions"][subscription]["count"] -= 1 + if not debug_info_entities[entity_id]["subscriptions"][subscription]["count"]: + debug_info_entities[entity_id]["subscriptions"].pop(subscription) -def add_entity_discovery_data(hass, discovery_data, entity_id): +def add_entity_discovery_data( + hass: HomeAssistant, discovery_data: DiscoveryInfoType, entity_id: str +) -> None: """Add discovery data.""" - debug_info = hass.data[DATA_MQTT_DEBUG_INFO] - entity_info = debug_info["entities"].setdefault( + entity_info = get_mqtt_data(hass).debug_info_entities.setdefault( entity_id, {"subscriptions": {}, "discovery_data": {}, "transmitted": {}} ) entity_info["discovery_data"] = discovery_data -def update_entity_discovery_data(hass, discovery_payload, entity_id): +def update_entity_discovery_data( + hass: HomeAssistant, discovery_payload: DiscoveryInfoType, entity_id: str +) -> None: """Update discovery data.""" - entity_info = hass.data[DATA_MQTT_DEBUG_INFO]["entities"][entity_id] - entity_info["discovery_data"][ATTR_DISCOVERY_PAYLOAD] = discovery_payload + assert ( + discovery_data := get_mqtt_data(hass).debug_info_entities[entity_id][ + "discovery_data" + ] + ) is not None + discovery_data[ATTR_DISCOVERY_PAYLOAD] = discovery_payload -def remove_entity_data(hass, entity_id): +def remove_entity_data(hass: HomeAssistant, entity_id: str) -> None: """Remove discovery data.""" - if entity_id in hass.data[DATA_MQTT_DEBUG_INFO]["entities"]: - hass.data[DATA_MQTT_DEBUG_INFO]["entities"].pop(entity_id) + if entity_id in (debug_info_entities := get_mqtt_data(hass).debug_info_entities): + debug_info_entities.pop(entity_id) -def add_trigger_discovery_data(hass, discovery_hash, discovery_data, device_id): +def add_trigger_discovery_data( + hass: HomeAssistant, + discovery_hash: tuple[str, str], + discovery_data: DiscoveryInfoType, + device_id: str, +) -> None: """Add discovery data.""" - debug_info = hass.data[DATA_MQTT_DEBUG_INFO] - debug_info["triggers"][discovery_hash] = { + get_mqtt_data(hass).debug_info_triggers[discovery_hash] = { "device_id": device_id, "discovery_data": discovery_data, } -def update_trigger_discovery_data(hass, discovery_hash, discovery_payload): +def update_trigger_discovery_data( + hass: HomeAssistant, + discovery_hash: tuple[str, str], + discovery_payload: DiscoveryInfoType, +) -> None: """Update discovery data.""" - trigger_info = hass.data[DATA_MQTT_DEBUG_INFO]["triggers"][discovery_hash] - trigger_info["discovery_data"][ATTR_DISCOVERY_PAYLOAD] = discovery_payload + get_mqtt_data(hass).debug_info_triggers[discovery_hash]["discovery_data"][ + ATTR_DISCOVERY_PAYLOAD + ] = discovery_payload -def remove_trigger_discovery_data(hass, discovery_hash): +def remove_trigger_discovery_data( + hass: HomeAssistant, discovery_hash: tuple[str, str] +) -> None: """Remove discovery data.""" - hass.data[DATA_MQTT_DEBUG_INFO]["triggers"].pop(discovery_hash) + get_mqtt_data(hass).debug_info_triggers.pop(discovery_hash) def _info_for_entity(hass: HomeAssistant, entity_id: str) -> dict[str, Any]: - mqtt_debug_info = hass.data[DATA_MQTT_DEBUG_INFO] - entity_info = mqtt_debug_info["entities"][entity_id] + entity_info = get_mqtt_data(hass).debug_info_entities[entity_id] subscriptions = [ { "topic": topic, @@ -205,9 +220,10 @@ def _info_for_entity(hass: HomeAssistant, entity_id: str) -> dict[str, Any]: } -def _info_for_trigger(hass: HomeAssistant, trigger_key: str) -> dict[str, Any]: - mqtt_debug_info = hass.data[DATA_MQTT_DEBUG_INFO] - trigger = mqtt_debug_info["triggers"][trigger_key] +def _info_for_trigger( + hass: HomeAssistant, trigger_key: tuple[str, str] +) -> dict[str, Any]: + trigger = get_mqtt_data(hass).debug_info_triggers[trigger_key] discovery_data = None if trigger["discovery_data"] is not None: discovery_data = { @@ -217,36 +233,39 @@ def _info_for_trigger(hass: HomeAssistant, trigger_key: str) -> dict[str, Any]: return {"discovery_data": discovery_data, "trigger_key": trigger_key} -def info_for_config_entry(hass): +def info_for_config_entry(hass: HomeAssistant) -> dict[str, list[Any]]: """Get debug info for all entities and triggers.""" - mqtt_info = {"entities": [], "triggers": []} - mqtt_debug_info = hass.data[DATA_MQTT_DEBUG_INFO] - for entity_id in mqtt_debug_info["entities"]: + mqtt_data = get_mqtt_data(hass) + mqtt_info: dict[str, list[Any]] = {"entities": [], "triggers": []} + + for entity_id in mqtt_data.debug_info_entities: mqtt_info["entities"].append(_info_for_entity(hass, entity_id)) - for trigger_key in mqtt_debug_info["triggers"]: + for trigger_key in mqtt_data.debug_info_triggers: mqtt_info["triggers"].append(_info_for_trigger(hass, trigger_key)) return mqtt_info -def info_for_device(hass, device_id): +def info_for_device(hass: HomeAssistant, device_id: str) -> dict[str, list[Any]]: """Get debug info for a device.""" - mqtt_info = {"entities": [], "triggers": []} + + mqtt_data = get_mqtt_data(hass) + + mqtt_info: dict[str, list[Any]] = {"entities": [], "triggers": []} entity_registry = er.async_get(hass) entries = er.async_entries_for_device( entity_registry, device_id, include_disabled_entities=True ) - mqtt_debug_info = hass.data[DATA_MQTT_DEBUG_INFO] for entry in entries: - if entry.entity_id not in mqtt_debug_info["entities"]: + if entry.entity_id not in mqtt_data.debug_info_entities: continue mqtt_info["entities"].append(_info_for_entity(hass, entry.entity_id)) - for trigger_key, trigger in mqtt_debug_info["triggers"].items(): + for trigger_key, trigger in mqtt_data.debug_info_triggers.items(): if trigger["device_id"] != device_id: continue diff --git a/homeassistant/components/mqtt/device_trigger.py b/homeassistant/components/mqtt/device_trigger.py index 7e37ed72821..f51731284cc 100644 --- a/homeassistant/components/mqtt/device_trigger.py +++ b/homeassistant/components/mqtt/device_trigger.py @@ -33,17 +33,16 @@ from .const import ( CONF_PAYLOAD, CONF_QOS, CONF_TOPIC, - DATA_MQTT, DOMAIN, ) from .discovery import MQTT_DISCOVERY_DONE from .mixins import ( MQTT_ENTITY_DEVICE_INFO_SCHEMA, - MqttData, MqttDiscoveryDeviceUpdate, send_discovery_done, update_device, ) +from .util import get_mqtt_data _LOGGER = logging.getLogger(__name__) @@ -203,7 +202,7 @@ class MqttDeviceTrigger(MqttDiscoveryDeviceUpdate): self.device_id = device_id self.discovery_data = discovery_data self.hass = hass - self._mqtt_data: MqttData = hass.data[DATA_MQTT] + self._mqtt_data = get_mqtt_data(hass) MqttDiscoveryDeviceUpdate.__init__( self, @@ -281,7 +280,7 @@ async def async_setup_trigger( async def async_removed_from_device(hass: HomeAssistant, device_id: str) -> None: """Handle Mqtt removed from a device.""" - mqtt_data: MqttData = hass.data[DATA_MQTT] + mqtt_data = get_mqtt_data(hass) triggers = await async_get_triggers(hass, device_id) for trig in triggers: device_trigger: Trigger = mqtt_data.device_triggers.pop(trig[CONF_DISCOVERY_ID]) @@ -296,7 +295,7 @@ async def async_get_triggers( hass: HomeAssistant, device_id: str ) -> list[dict[str, str]]: """List device triggers for MQTT devices.""" - mqtt_data: MqttData = hass.data[DATA_MQTT] + mqtt_data = get_mqtt_data(hass) triggers: list[dict[str, str]] = [] if not mqtt_data.device_triggers: @@ -325,7 +324,7 @@ async def async_attach_trigger( trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Attach a trigger.""" - mqtt_data: MqttData = hass.data[DATA_MQTT] + mqtt_data = get_mqtt_data(hass) device_id = config[CONF_DEVICE_ID] discovery_id = config[CONF_DISCOVERY_ID] diff --git a/homeassistant/components/mqtt/diagnostics.py b/homeassistant/components/mqtt/diagnostics.py index 2a6322cac63..173c583ca6a 100644 --- a/homeassistant/components/mqtt/diagnostics.py +++ b/homeassistant/components/mqtt/diagnostics.py @@ -16,7 +16,8 @@ from homeassistant.core import HomeAssistant, callback, split_entity_id from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.device_registry import DeviceEntry -from . import DATA_MQTT, MQTT, debug_info, is_connected +from . import debug_info, is_connected +from .util import get_mqtt_data REDACT_CONFIG = {CONF_PASSWORD, CONF_USERNAME} REDACT_STATE_DEVICE_TRACKER = {ATTR_LATITUDE, ATTR_LONGITUDE} @@ -43,7 +44,8 @@ def _async_get_diagnostics( device: DeviceEntry | None = None, ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - mqtt_instance: MQTT = hass.data[DATA_MQTT].client + mqtt_instance = get_mqtt_data(hass).client + assert mqtt_instance is not None redacted_config = async_redact_data(mqtt_instance.conf, REDACT_CONFIG) diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index 65051ce54fc..23453e146ed 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -7,8 +7,8 @@ import functools import logging import re import time -from typing import TYPE_CHECKING +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_DEVICE, CONF_PLATFORM from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -19,6 +19,7 @@ from homeassistant.helpers.dispatcher import ( ) from homeassistant.helpers.json import json_loads from homeassistant.helpers.service_info.mqtt import MqttServiceInfo +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.loader import async_get_mqtt from .. import mqtt @@ -29,12 +30,10 @@ from .const import ( ATTR_DISCOVERY_TOPIC, CONF_AVAILABILITY, CONF_TOPIC, - DATA_MQTT, DOMAIN, ) - -if TYPE_CHECKING: - from .mixins import MqttData +from .models import ReceiveMessage +from .util import get_mqtt_data _LOGGER = logging.getLogger(__name__) @@ -66,11 +65,6 @@ SUPPORTED_COMPONENTS = [ "vacuum", ] -ALREADY_DISCOVERED = "mqtt_discovered_components" -PENDING_DISCOVERED = "mqtt_pending_components" -DATA_CONFIG_FLOW_LOCK = "mqtt_discovery_config_flow_lock" -DISCOVERY_UNSUBSCRIBE = "mqtt_discovery_unsubscribe" -INTEGRATION_UNSUBSCRIBE = "mqtt_integration_discovery_unsubscribe" MQTT_DISCOVERY_UPDATED = "mqtt_discovery_updated_{}" MQTT_DISCOVERY_NEW = "mqtt_discovery_new_{}_{}" MQTT_DISCOVERY_DONE = "mqtt_discovery_done_{}" @@ -81,24 +75,24 @@ TOPIC_BASE = "~" class MQTTConfig(dict): """Dummy class to allow adding attributes.""" - discovery_data: dict + discovery_data: DiscoveryInfoType def clear_discovery_hash(hass: HomeAssistant, discovery_hash: tuple[str, str]) -> None: - """Clear entry in ALREADY_DISCOVERED list.""" - del hass.data[ALREADY_DISCOVERED][discovery_hash] + """Clear entry from already discovered list.""" + get_mqtt_data(hass).discovery_already_discovered.remove(discovery_hash) -def set_discovery_hash(hass: HomeAssistant, discovery_hash: tuple[str, str]): - """Clear entry in ALREADY_DISCOVERED list.""" - hass.data[ALREADY_DISCOVERED][discovery_hash] = {} +def set_discovery_hash(hass: HomeAssistant, discovery_hash: tuple[str, str]) -> None: + """Add entry to already discovered list.""" + get_mqtt_data(hass).discovery_already_discovered.add(discovery_hash) async def async_start( # noqa: C901 - hass: HomeAssistant, discovery_topic, config_entry=None + hass: HomeAssistant, discovery_topic: str, config_entry: ConfigEntry ) -> None: """Start MQTT Discovery.""" - mqtt_data: MqttData = hass.data[DATA_MQTT] + mqtt_data = get_mqtt_data(hass) mqtt_integrations = {} async def async_discovery_message_received(msg): @@ -179,8 +173,8 @@ async def async_start( # noqa: C901 payload[CONF_PLATFORM] = "mqtt" - if discovery_hash in hass.data[PENDING_DISCOVERED]: - pending = hass.data[PENDING_DISCOVERED][discovery_hash]["pending"] + if discovery_hash in mqtt_data.discovery_pending_discovered: + pending = mqtt_data.discovery_pending_discovered[discovery_hash]["pending"] pending.appendleft(payload) _LOGGER.info( "Component has already been discovered: %s %s, queuing update", @@ -191,27 +185,31 @@ async def async_start( # noqa: C901 await async_process_discovery_payload(component, discovery_id, payload) - async def async_process_discovery_payload(component, discovery_id, payload): + async def async_process_discovery_payload( + component: str, discovery_id: str, payload: ConfigType + ) -> None: """Process the payload of a new discovery.""" _LOGGER.debug("Process discovery payload %s", payload) discovery_hash = (component, discovery_id) - if discovery_hash in hass.data[ALREADY_DISCOVERED] or payload: + if discovery_hash in mqtt_data.discovery_already_discovered or payload: - async def discovery_done(_): - pending = hass.data[PENDING_DISCOVERED][discovery_hash]["pending"] + async def discovery_done(_) -> None: + pending = mqtt_data.discovery_pending_discovered[discovery_hash][ + "pending" + ] _LOGGER.debug("Pending discovery for %s: %s", discovery_hash, pending) if not pending: - hass.data[PENDING_DISCOVERED][discovery_hash]["unsub"]() - hass.data[PENDING_DISCOVERED].pop(discovery_hash) + mqtt_data.discovery_pending_discovered[discovery_hash]["unsub"]() + mqtt_data.discovery_pending_discovered.pop(discovery_hash) else: payload = pending.pop() await async_process_discovery_payload( component, discovery_id, payload ) - if discovery_hash not in hass.data[PENDING_DISCOVERED]: - hass.data[PENDING_DISCOVERED][discovery_hash] = { + if discovery_hash not in mqtt_data.discovery_pending_discovered: + mqtt_data.discovery_pending_discovered[discovery_hash] = { "unsub": async_dispatcher_connect( hass, MQTT_DISCOVERY_DONE.format(discovery_hash), @@ -220,7 +218,7 @@ async def async_start( # noqa: C901 "pending": deque([]), } - if discovery_hash in hass.data[ALREADY_DISCOVERED]: + if discovery_hash in mqtt_data.discovery_already_discovered: # Dispatch update _LOGGER.info( "Component has already been discovered: %s %s, sending update", @@ -233,7 +231,7 @@ async def async_start( # noqa: C901 elif payload: # Add component _LOGGER.info("Found new component: %s %s", component, discovery_id) - hass.data[ALREADY_DISCOVERED][discovery_hash] = None + mqtt_data.discovery_already_discovered.add(discovery_hash) async_dispatcher_send( hass, MQTT_DISCOVERY_NEW.format(component, "mqtt"), payload ) @@ -243,15 +241,11 @@ async def async_start( # noqa: C901 hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None ) - hass.data.setdefault(DATA_CONFIG_FLOW_LOCK, asyncio.Lock()) - hass.data[ALREADY_DISCOVERED] = {} - hass.data[PENDING_DISCOVERED] = {} - discovery_topics = [ f"{discovery_topic}/+/+/config", f"{discovery_topic}/+/+/+/config", ] - hass.data[DISCOVERY_UNSUBSCRIBE] = await asyncio.gather( + mqtt_data.discovery_unsubscribe = await asyncio.gather( *( mqtt.async_subscribe(hass, topic, async_discovery_message_received, 0) for topic in discovery_topics @@ -261,19 +255,20 @@ async def async_start( # noqa: C901 mqtt_data.last_discovery = time.time() mqtt_integrations = await async_get_mqtt(hass) - hass.data[INTEGRATION_UNSUBSCRIBE] = {} - for (integration, topics) in mqtt_integrations.items(): - async def async_integration_message_received(integration, msg): + async def async_integration_message_received( + integration: str, msg: ReceiveMessage + ) -> None: """Process the received message.""" + assert mqtt_data.data_config_flow_lock key = f"{integration}_{msg.subscribed_topic}" # Lock to prevent initiating many parallel config flows. # Note: The lock is not intended to prevent a race, only for performance - async with hass.data[DATA_CONFIG_FLOW_LOCK]: + async with mqtt_data.data_config_flow_lock: # Already unsubscribed - if key not in hass.data[INTEGRATION_UNSUBSCRIBE]: + if key not in mqtt_data.integration_unsubscribe: return data = MqttServiceInfo( @@ -293,14 +288,14 @@ async def async_start( # noqa: C901 and result["reason"] in ("already_configured", "single_instance_allowed") ): - unsub = hass.data[INTEGRATION_UNSUBSCRIBE].pop(key, None) + unsub = mqtt_data.integration_unsubscribe.pop(key, None) if unsub is None: return unsub() for topic in topics: key = f"{integration}_{topic}" - hass.data[INTEGRATION_UNSUBSCRIBE][key] = await mqtt.async_subscribe( + mqtt_data.integration_unsubscribe[key] = await mqtt.async_subscribe( hass, topic, functools.partial(async_integration_message_received, integration), @@ -310,11 +305,10 @@ async def async_start( # noqa: C901 async def async_stop(hass: HomeAssistant) -> None: """Stop MQTT Discovery.""" - if DISCOVERY_UNSUBSCRIBE in hass.data: - for unsub in hass.data[DISCOVERY_UNSUBSCRIBE]: - unsub() - hass.data[DISCOVERY_UNSUBSCRIBE] = [] - if INTEGRATION_UNSUBSCRIBE in hass.data: - for key, unsub in list(hass.data[INTEGRATION_UNSUBSCRIBE].items()): - unsub() - hass.data[INTEGRATION_UNSUBSCRIBE].pop(key) + mqtt_data = get_mqtt_data(hass) + for unsub in mqtt_data.discovery_unsubscribe: + unsub() + mqtt_data.discovery_unsubscribe = [] + for key, unsub in list(mqtt_data.integration_unsubscribe.items()): + unsub() + mqtt_data.integration_unsubscribe.pop(key) diff --git a/homeassistant/components/mqtt/light/schema_json.py b/homeassistant/components/mqtt/light/schema_json.py index 14eca9569f8..8843e8542eb 100644 --- a/homeassistant/components/mqtt/light/schema_json.py +++ b/homeassistant/components/mqtt/light/schema_json.py @@ -20,14 +20,13 @@ from homeassistant.components.light import ( ENTITY_ID_FORMAT, FLASH_LONG, FLASH_SHORT, - SUPPORT_BRIGHTNESS, - SUPPORT_COLOR, - SUPPORT_COLOR_TEMP, VALID_COLOR_MODES, ColorMode, LightEntity, LightEntityFeature, - legacy_supported_features, + brightness_supported, + color_supported, + filter_supported_color_modes, valid_supported_color_modes, ) from homeassistant.const import ( @@ -198,6 +197,7 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): self._color_mode = None self._color_temp = None self._effect = None + self._fixed_color_mode = None self._flash_times = None self._hs = None self._rgb = None @@ -230,13 +230,20 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): ) self._supported_features |= config[CONF_EFFECT] and LightEntityFeature.EFFECT if not self._config[CONF_COLOR_MODE]: - self._supported_features |= config[CONF_BRIGHTNESS] and SUPPORT_BRIGHTNESS - self._supported_features |= config[CONF_COLOR_TEMP] and SUPPORT_COLOR_TEMP - self._supported_features |= config[CONF_HS] and SUPPORT_COLOR - self._supported_features |= config[CONF_RGB] and ( - SUPPORT_COLOR | SUPPORT_BRIGHTNESS - ) - self._supported_features |= config[CONF_XY] and SUPPORT_COLOR + color_modes = {ColorMode.ONOFF} + if config[CONF_BRIGHTNESS]: + color_modes.add(ColorMode.BRIGHTNESS) + if config[CONF_COLOR_TEMP]: + color_modes.add(ColorMode.COLOR_TEMP) + if config[CONF_HS] or config[CONF_RGB] or config[CONF_XY]: + color_modes.add(ColorMode.HS) + self._supported_color_modes = filter_supported_color_modes(color_modes) + if len(self._supported_color_modes) == 1: + self._fixed_color_mode = next(iter(self._supported_color_modes)) + else: + self._supported_color_modes = self._config[CONF_SUPPORTED_COLOR_MODES] + if len(self._supported_color_modes) == 1: + self._color_mode = next(iter(self._supported_color_modes)) def _update_color(self, values): if not self._config[CONF_COLOR_MODE]: @@ -343,7 +350,12 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): elif values["state"] is None: self._state = None - if self._supported_features and SUPPORT_COLOR and "color" in values: + if ( + not self._config[CONF_COLOR_MODE] + and color_supported(self._supported_color_modes) + and "color" in values + ): + # Deprecated color handling if values["color"] is None: self._hs = None else: @@ -352,7 +364,7 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): if self._config[CONF_COLOR_MODE] and "color_mode" in values: self._update_color(values) - if self._supported_features and SUPPORT_BRIGHTNESS: + if brightness_supported(self._supported_color_modes): try: self._brightness = int( values["brightness"] @@ -368,10 +380,10 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): ) if ( - self._supported_features - and SUPPORT_COLOR_TEMP + ColorMode.COLOR_TEMP in self._supported_color_modes and not self._config[CONF_COLOR_MODE] ): + # Deprecated color handling try: if values["color_temp"] is None: self._color_temp = None @@ -491,19 +503,25 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): @property def color_mode(self): """Return current color mode.""" - return self._color_mode + if self._config[CONF_COLOR_MODE]: + return self._color_mode + if self._fixed_color_mode: + # Legacy light with support for a single color mode + return self._fixed_color_mode + # Legacy light with support for ct + hs, prioritize hs + if self._hs is not None: + return ColorMode.HS + return ColorMode.COLOR_TEMP @property def supported_color_modes(self): """Flag supported color modes.""" - return self._config.get(CONF_SUPPORTED_COLOR_MODES) + return self._supported_color_modes @property def supported_features(self): """Flag supported features.""" - return legacy_supported_features( - self._supported_features, self._config.get(CONF_SUPPORTED_COLOR_MODES) - ) + return self._supported_features def _set_flash_and_transition(self, message, **kwargs): if ATTR_TRANSITION in kwargs: @@ -527,7 +545,10 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): return tuple(round(i / 255 * brightness) for i in rgbxx) def _supports_color_mode(self, color_mode): - return self.supported_color_modes and color_mode in self.supported_color_modes + """Return True if the light natively supports a color mode.""" + return ( + self._config[CONF_COLOR_MODE] and color_mode in self.supported_color_modes + ) async def async_turn_on(self, **kwargs): # noqa: C901 """Turn the device on. @@ -541,6 +562,7 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): if ATTR_HS_COLOR in kwargs and ( self._config[CONF_HS] or self._config[CONF_RGB] or self._config[CONF_XY] ): + # Legacy color handling hs_color = kwargs[ATTR_HS_COLOR] message["color"] = {} if self._config[CONF_RGB]: @@ -565,6 +587,7 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): message["color"]["s"] = hs_color[1] if self._optimistic: + self._color_temp = None self._hs = kwargs[ATTR_HS_COLOR] should_update = True @@ -634,7 +657,9 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): message["color_temp"] = int(kwargs[ATTR_COLOR_TEMP]) if self._optimistic: + self._color_mode = ColorMode.COLOR_TEMP self._color_temp = kwargs[ATTR_COLOR_TEMP] + self._hs = None should_update = True if ATTR_EFFECT in kwargs: diff --git a/homeassistant/components/mqtt/light/schema_template.py b/homeassistant/components/mqtt/light/schema_template.py index 73f2786ad12..dacc977a036 100644 --- a/homeassistant/components/mqtt/light/schema_template.py +++ b/homeassistant/components/mqtt/light/schema_template.py @@ -11,11 +11,10 @@ from homeassistant.components.light import ( ATTR_HS_COLOR, ATTR_TRANSITION, ENTITY_ID_FORMAT, - SUPPORT_BRIGHTNESS, - SUPPORT_COLOR, - SUPPORT_COLOR_TEMP, + ColorMode, LightEntity, LightEntityFeature, + filter_supported_color_modes, ) from homeassistant.const import ( CONF_NAME, @@ -129,6 +128,7 @@ class MqttLightTemplate(MqttEntity, LightEntity, RestoreEntity): # features self._brightness = None + self._fixed_color_mode = None self._color_temp = None self._hs = None self._effect = None @@ -166,6 +166,21 @@ class MqttLightTemplate(MqttEntity, LightEntity, RestoreEntity): or self._templates[CONF_STATE_TEMPLATE] is None ) + color_modes = {ColorMode.ONOFF} + if self._templates[CONF_BRIGHTNESS_TEMPLATE] is not None: + color_modes.add(ColorMode.BRIGHTNESS) + if self._templates[CONF_COLOR_TEMP_TEMPLATE] is not None: + color_modes.add(ColorMode.COLOR_TEMP) + if ( + self._templates[CONF_RED_TEMPLATE] is not None + and self._templates[CONF_GREEN_TEMPLATE] is not None + and self._templates[CONF_BLUE_TEMPLATE] is not None + ): + color_modes.add(ColorMode.HS) + self._supported_color_modes = filter_supported_color_modes(color_modes) + if len(self._supported_color_modes) == 1: + self._fixed_color_mode = next(iter(self._supported_color_modes)) + def _prepare_subscribe_topics(self): """(Re)Subscribe to topics.""" for tpl in self._templates.values(): @@ -200,11 +215,10 @@ class MqttLightTemplate(MqttEntity, LightEntity, RestoreEntity): if self._templates[CONF_COLOR_TEMP_TEMPLATE] is not None: try: - self._color_temp = int( - self._templates[ - CONF_COLOR_TEMP_TEMPLATE - ].async_render_with_possible_json_value(msg.payload) - ) + color_temp = self._templates[ + CONF_COLOR_TEMP_TEMPLATE + ].async_render_with_possible_json_value(msg.payload) + self._color_temp = int(color_temp) if color_temp != "None" else None except ValueError: _LOGGER.warning("Invalid color temperature value received") @@ -214,22 +228,21 @@ class MqttLightTemplate(MqttEntity, LightEntity, RestoreEntity): and self._templates[CONF_BLUE_TEMPLATE] is not None ): try: - red = int( - self._templates[ - CONF_RED_TEMPLATE - ].async_render_with_possible_json_value(msg.payload) - ) - green = int( - self._templates[ - CONF_GREEN_TEMPLATE - ].async_render_with_possible_json_value(msg.payload) - ) - blue = int( - self._templates[ - CONF_BLUE_TEMPLATE - ].async_render_with_possible_json_value(msg.payload) - ) - self._hs = color_util.color_RGB_to_hs(red, green, blue) + red = self._templates[ + CONF_RED_TEMPLATE + ].async_render_with_possible_json_value(msg.payload) + green = self._templates[ + CONF_GREEN_TEMPLATE + ].async_render_with_possible_json_value(msg.payload) + blue = self._templates[ + CONF_BLUE_TEMPLATE + ].async_render_with_possible_json_value(msg.payload) + if red == "None" and green == "None" and blue == "None": + self._hs = None + else: + self._hs = color_util.color_RGB_to_hs( + int(red), int(green), int(blue) + ) except ValueError: _LOGGER.warning("Invalid color value received") @@ -340,6 +353,7 @@ class MqttLightTemplate(MqttEntity, LightEntity, RestoreEntity): if self._optimistic: self._color_temp = kwargs[ATTR_COLOR_TEMP] + self._hs = None if ATTR_HS_COLOR in kwargs: hs_color = kwargs[ATTR_HS_COLOR] @@ -363,6 +377,7 @@ class MqttLightTemplate(MqttEntity, LightEntity, RestoreEntity): values["sat"] = hs_color[1] if self._optimistic: + self._color_temp = None self._hs = kwargs[ATTR_HS_COLOR] if ATTR_EFFECT in kwargs: @@ -415,21 +430,26 @@ class MqttLightTemplate(MqttEntity, LightEntity, RestoreEntity): if self._optimistic: self.async_write_ha_state() + @property + def color_mode(self): + """Return current color mode.""" + if self._fixed_color_mode: + return self._fixed_color_mode + # Support for ct + hs, prioritize hs + if self._hs is not None: + return ColorMode.HS + return ColorMode.COLOR_TEMP + + @property + def supported_color_modes(self): + """Flag supported color modes.""" + return self._supported_color_modes + @property def supported_features(self): """Flag supported features.""" features = LightEntityFeature.FLASH | LightEntityFeature.TRANSITION - if self._templates[CONF_BRIGHTNESS_TEMPLATE] is not None: - features = features | SUPPORT_BRIGHTNESS - if ( - self._templates[CONF_RED_TEMPLATE] is not None - and self._templates[CONF_GREEN_TEMPLATE] is not None - and self._templates[CONF_BLUE_TEMPLATE] is not None - ): - features = features | SUPPORT_COLOR | SUPPORT_BRIGHTNESS if self._config.get(CONF_EFFECT_LIST) is not None: features = features | LightEntityFeature.EFFECT - if self._templates[CONF_COLOR_TEMP_TEMPLATE] is not None: - features = features | SUPPORT_COLOR_TEMP return features diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index 477be399e26..8022a6e91ae 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -4,10 +4,9 @@ from __future__ import annotations from abc import abstractmethod import asyncio from collections.abc import Callable, Coroutine -from dataclasses import dataclass, field from functools import partial import logging -from typing import TYPE_CHECKING, Any, Protocol, cast, final +from typing import Any, Protocol, cast, final import voluptuous as vol @@ -29,13 +28,7 @@ from homeassistant.const import ( CONF_UNIQUE_ID, CONF_VALUE_TEMPLATE, ) -from homeassistant.core import ( - CALLBACK_TYPE, - Event, - HomeAssistant, - async_get_hass, - callback, -) +from homeassistant.core import Event, HomeAssistant, async_get_hass, callback from homeassistant.helpers import ( config_validation as cv, device_registry as dr, @@ -60,7 +53,7 @@ from homeassistant.helpers.json import json_loads from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import debug_info, subscription -from .client import MQTT, Subscription, async_publish +from .client import async_publish from .const import ( ATTR_DISCOVERY_HASH, ATTR_DISCOVERY_PAYLOAD, @@ -69,7 +62,6 @@ from .const import ( CONF_ENCODING, CONF_QOS, CONF_TOPIC, - DATA_MQTT, DEFAULT_ENCODING, DEFAULT_PAYLOAD_AVAILABLE, DEFAULT_PAYLOAD_NOT_AVAILABLE, @@ -91,10 +83,7 @@ from .subscription import ( async_subscribe_topics, async_unsubscribe_topics, ) -from .util import mqtt_config_entry_enabled, valid_subscribe_topic - -if TYPE_CHECKING: - from .device_trigger import Trigger +from .util import get_mqtt_data, mqtt_config_entry_enabled, valid_subscribe_topic _LOGGER = logging.getLogger(__name__) @@ -272,27 +261,6 @@ def warn_for_legacy_schema(domain: str) -> Callable: return validator -@dataclass -class MqttData: - """Keep the MQTT entry data.""" - - client: MQTT | None = None - config: ConfigType | None = None - device_triggers: dict[str, Trigger] = field(default_factory=dict) - discovery_registry_hooks: dict[tuple[str, str], CALLBACK_TYPE] = field( - default_factory=dict - ) - last_discovery: float = 0.0 - reload_dispatchers: list[CALLBACK_TYPE] = field(default_factory=list) - reload_entry: bool = False - reload_handlers: dict[str, Callable[[], Coroutine[Any, Any, None]]] = field( - default_factory=dict - ) - reload_needed: bool = False - subscriptions_to_restore: list[Subscription] = field(default_factory=list) - updated_config: ConfigType = field(default_factory=dict) - - class SetupEntity(Protocol): """Protocol type for async_setup_entities.""" @@ -313,8 +281,7 @@ async def async_get_platform_config_from_yaml( config_yaml: ConfigType | None = None, ) -> list[ConfigType]: """Return a list of validated configurations for the domain.""" - - mqtt_data: MqttData = hass.data[DATA_MQTT] + mqtt_data = get_mqtt_data(hass) if config_yaml is None: config_yaml = mqtt_data.config if not config_yaml: @@ -331,7 +298,7 @@ async def async_setup_entry_helper( discovery_schema: vol.Schema, ) -> None: """Set up entity, automation or tag creation dynamically through MQTT discovery.""" - mqtt_data: MqttData = hass.data[DATA_MQTT] + mqtt_data = get_mqtt_data(hass) async def async_discover(discovery_payload): """Discover and add an MQTT entity, automation or tag.""" @@ -363,7 +330,7 @@ async def async_setup_entry_helper( async def _async_setup_entities() -> None: """Set up MQTT items from configuration.yaml.""" - mqtt_data: MqttData = hass.data[DATA_MQTT] + mqtt_data = get_mqtt_data(hass) if mqtt_data.updated_config: # The platform has been reloaded config_yaml = mqtt_data.updated_config @@ -395,7 +362,7 @@ async def async_setup_platform_helper( async_setup_entities: SetupEntity, ) -> None: """Help to set up the platform for manual configured MQTT entities.""" - mqtt_data: MqttData = hass.data[DATA_MQTT] + mqtt_data = get_mqtt_data(hass) if mqtt_data.reload_entry: _LOGGER.debug( "MQTT integration is %s, skipping setup of manually configured MQTT items while unloading the config entry", @@ -621,7 +588,7 @@ class MqttAvailability(Entity): @property def available(self) -> bool: """Return if the device is available.""" - mqtt_data: MqttData = self.hass.data[DATA_MQTT] + mqtt_data = get_mqtt_data(self.hass) assert mqtt_data.client is not None client = mqtt_data.client if not client.connected and not self.hass.is_stopping: @@ -844,7 +811,7 @@ class MqttDiscoveryUpdate(Entity): self._removed_from_hass = False if discovery_data is None: return - mqtt_data: MqttData = hass.data[DATA_MQTT] + mqtt_data = get_mqtt_data(hass) self._registry_hooks = mqtt_data.discovery_registry_hooks discovery_hash: tuple[str, str] = discovery_data[ATTR_DISCOVERY_HASH] if discovery_hash in self._registry_hooks: @@ -898,6 +865,7 @@ class MqttDiscoveryUpdate(Entity): 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 ) diff --git a/homeassistant/components/mqtt/models.py b/homeassistant/components/mqtt/models.py index d40b882d81b..b7cb81b2ea4 100644 --- a/homeassistant/components/mqtt/models.py +++ b/homeassistant/components/mqtt/models.py @@ -2,21 +2,34 @@ from __future__ import annotations from ast import literal_eval +import asyncio +from collections import deque from collections.abc import Callable, Coroutine +from dataclasses import dataclass, field import datetime as dt -from typing import Any, Union +import logging +from typing import TYPE_CHECKING, Any, TypedDict, Union import attr from homeassistant.const import ATTR_ENTITY_ID, ATTR_NAME -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.helpers import template from homeassistant.helpers.entity import Entity from homeassistant.helpers.service_info.mqtt import ReceivePayloadType -from homeassistant.helpers.typing import TemplateVarsType +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, TemplateVarsType + +if TYPE_CHECKING: + from .client import MQTT, Subscription + from .debug_info import TimestampedPublishMessage + from .device_trigger import Trigger + from .discovery import MQTTConfig + from .tag import MQTTTagScanner _SENTINEL = object() +_LOGGER = logging.getLogger(__name__) + ATTR_THIS = "this" PublishPayloadType = Union[str, bytes, int, float, None] @@ -48,6 +61,35 @@ AsyncMessageCallbackType = Callable[[ReceiveMessage], Coroutine[Any, Any, None]] MessageCallbackType = Callable[[ReceiveMessage], None] +class SubscriptionDebugInfo(TypedDict): + """Class for holding subscription debug info.""" + + messages: deque[ReceiveMessage] + count: int + + +class EntityDebugInfo(TypedDict): + """Class for holding entity based debug info.""" + + subscriptions: dict[str, SubscriptionDebugInfo] + discovery_data: DiscoveryInfoType + transmitted: dict[str, dict[str, deque[TimestampedPublishMessage]]] + + +class TriggerDebugInfo(TypedDict): + """Class for holding trigger based debug info.""" + + device_id: str + discovery_data: DiscoveryInfoType + + +class PendingDiscovered(TypedDict): + """Pending discovered items.""" + + pending: deque[MQTTConfig] + unsub: CALLBACK_TYPE + + class MqttCommandTemplate: """Class for rendering MQTT payload with command templates.""" @@ -109,6 +151,11 @@ class MqttCommandTemplate: if variables is not None: values.update(variables) + _LOGGER.debug( + "Rendering outgoing payload with variables %s and %s", + values, + self._command_template, + ) return _convert_outgoing_payload( self._command_template.async_render(values, parse_result=False) ) @@ -167,10 +214,56 @@ class MqttValueTemplate: values[ATTR_THIS] = self._template_state if default == _SENTINEL: + _LOGGER.debug( + "Rendering incoming payload '%s' with variables %s and %s", + payload, + values, + self._value_template, + ) return self._value_template.async_render_with_possible_json_value( payload, variables=values ) + _LOGGER.debug( + "Rendering incoming payload '%s' with variables %s with default value '%s' and %s", + payload, + values, + default, + self._value_template, + ) return self._value_template.async_render_with_possible_json_value( payload, default, variables=values ) + + +@dataclass +class MqttData: + """Keep the MQTT entry data.""" + + client: MQTT | None = None + config: ConfigType | None = None + debug_info_entities: dict[str, EntityDebugInfo] = field(default_factory=dict) + debug_info_triggers: dict[tuple[str, str], TriggerDebugInfo] = field( + default_factory=dict + ) + device_triggers: dict[str, Trigger] = field(default_factory=dict) + data_config_flow_lock: asyncio.Lock = field(default_factory=asyncio.Lock) + discovery_already_discovered: set[tuple[str, str]] = field(default_factory=set) + discovery_pending_discovered: dict[tuple[str, str], PendingDiscovered] = field( + default_factory=dict + ) + discovery_registry_hooks: dict[tuple[str, str], CALLBACK_TYPE] = field( + default_factory=dict + ) + discovery_unsubscribe: list[CALLBACK_TYPE] = field(default_factory=list) + integration_unsubscribe: dict[str, CALLBACK_TYPE] = field(default_factory=dict) + last_discovery: float = 0.0 + reload_dispatchers: list[CALLBACK_TYPE] = field(default_factory=list) + reload_entry: bool = False + reload_handlers: dict[str, Callable[[], Coroutine[Any, Any, None]]] = field( + default_factory=dict + ) + reload_needed: bool = False + subscriptions_to_restore: list[Subscription] = field(default_factory=list) + tags: dict[str, dict[str, MQTTTagScanner]] = field(default_factory=dict) + updated_config: ConfigType = field(default_factory=dict) diff --git a/homeassistant/components/mqtt/scene.py b/homeassistant/components/mqtt/scene.py index 861bdd14f6e..e237d70e903 100644 --- a/homeassistant/components/mqtt/scene.py +++ b/homeassistant/components/mqtt/scene.py @@ -2,6 +2,7 @@ from __future__ import annotations import functools +from typing import Any import voluptuous as vol @@ -121,7 +122,7 @@ class MqttScene( async def _subscribe_topics(self): """(Re)Subscribe to topics.""" - async def async_activate(self, **kwargs): + async def async_activate(self, **kwargs: Any) -> None: """Activate the scene. This method is a coroutine. diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index e95979bbaca..7c98fdf51b7 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -229,6 +229,7 @@ class MqttSensor(MqttEntity, RestoreSensor): def _setup_from_config(self, config): """(Re)Setup the entity.""" + self._attr_force_update = config[CONF_FORCE_UPDATE] self._template = MqttValueTemplate( self._config.get(CONF_VALUE_TEMPLATE), entity=self ).async_render_with_possible_json_value @@ -346,11 +347,6 @@ class MqttSensor(MqttEntity, RestoreSensor): """Return the unit this state is expressed in.""" return self._config.get(CONF_UNIT_OF_MEASUREMENT) - @property - def force_update(self) -> bool: - """Force update.""" - return self._config[CONF_FORCE_UPDATE] - @property def native_value(self) -> StateType | datetime: """Return the state of the entity.""" diff --git a/homeassistant/components/mqtt/siren.py b/homeassistant/components/mqtt/siren.py index ebb08919789..c8332046092 100644 --- a/homeassistant/components/mqtt/siren.py +++ b/homeassistant/components/mqtt/siren.py @@ -10,16 +10,14 @@ import voluptuous as vol from homeassistant.components import siren from homeassistant.components.siren import ( - TURN_ON_SCHEMA, - SirenEntity, - SirenEntityFeature, - process_turn_on_params, -) -from homeassistant.components.siren.const import ( ATTR_AVAILABLE_TONES, ATTR_DURATION, ATTR_TONE, ATTR_VOLUME_LEVEL, + TURN_ON_SCHEMA, + SirenEntity, + SirenEntityFeature, + process_turn_on_params, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( diff --git a/homeassistant/components/mqtt/tag.py b/homeassistant/components/mqtt/tag.py index dc4cc0e109d..23afae35cc9 100644 --- a/homeassistant/components/mqtt/tag.py +++ b/homeassistant/components/mqtt/tag.py @@ -23,12 +23,11 @@ from .mixins import ( ) from .models import MqttValueTemplate, ReceiveMessage from .subscription import EntitySubscription -from .util import valid_subscribe_topic +from .util import get_mqtt_data, valid_subscribe_topic LOG_NAME = "Tag" TAG = "tag" -TAGS = "mqtt_tags" PLATFORM_SCHEMA = MQTT_BASE_SCHEMA.extend( { @@ -59,9 +58,8 @@ async def _async_setup_tag( discovery_id = discovery_hash[1] device_id = update_device(hass, config_entry, config) - hass.data.setdefault(TAGS, {}) - if device_id not in hass.data[TAGS]: - hass.data[TAGS][device_id] = {} + if device_id is not None and device_id not in (tags := get_mqtt_data(hass).tags): + tags[device_id] = {} tag_scanner = MQTTTagScanner( hass, @@ -74,16 +72,16 @@ async def _async_setup_tag( await tag_scanner.subscribe_topics() if device_id: - hass.data[TAGS][device_id][discovery_id] = tag_scanner + tags[device_id][discovery_id] = tag_scanner send_discovery_done(hass, discovery_data) def async_has_tags(hass: HomeAssistant, device_id: str) -> bool: """Device has tag scanners.""" - if TAGS not in hass.data or device_id not in hass.data[TAGS]: + if device_id not in (tags := get_mqtt_data(hass).tags): return False - return hass.data[TAGS][device_id] != {} + return tags[device_id] != {} class MQTTTagScanner(MqttDiscoveryDeviceUpdate): @@ -159,4 +157,4 @@ class MQTTTagScanner(MqttDiscoveryDeviceUpdate): self.hass, self._sub_state ) if self.device_id: - self.hass.data[TAGS][self.device_id].pop(discovery_id) + get_mqtt_data(self.hass).tags[self.device_id].pop(discovery_id) diff --git a/homeassistant/components/mqtt/translations/ca.json b/homeassistant/components/mqtt/translations/ca.json index 569adc2e423..43ab994f4d8 100644 --- a/homeassistant/components/mqtt/translations/ca.json +++ b/homeassistant/components/mqtt/translations/ca.json @@ -49,6 +49,12 @@ "button_triple_press": "\"{subtype}\" clicat tres vegades" } }, + "issues": { + "deprecated_yaml": { + "description": "S'ha trobat el MQTT {platform}(s) sota el codi de la integraci\u00f3 `{platform}`.\n\nSi us plau, moveu la configuraci\u00f3 a la integraci\u00f3 `mqtt`i reinicieu el Home Assistant per solucionar aquesta incid\u00e8ncia. Vegeu la [documentaci\u00f3]({more_info_url}) per a m\u00e9s informaci\u00f3.", + "title": "El MQTT {platform}(s) configurat manualment necessita la vostra atenci\u00f3" + } + }, "options": { "error": { "bad_birth": "Topic del missatge de naixement inv\u00e0lid.", diff --git a/homeassistant/components/mqtt/translations/fr.json b/homeassistant/components/mqtt/translations/fr.json index b84aec15a80..87ce8c2bdee 100644 --- a/homeassistant/components/mqtt/translations/fr.json +++ b/homeassistant/components/mqtt/translations/fr.json @@ -49,6 +49,11 @@ "button_triple_press": "\u00ab\u00a0{subtype}\u00a0\u00bb triple-cliqu\u00e9" } }, + "issues": { + "deprecated_yaml": { + "title": "Un\u00b7e ou plusieurs {plateform}\u00b7e\u00b7s MQTT configur\u00e9\u00b7e\u00b7s manuellement requi\u00e8rent votre attention" + } + }, "options": { "error": { "bad_birth": "Sujet de la naissance non valide.", diff --git a/homeassistant/components/mqtt/translations/he.json b/homeassistant/components/mqtt/translations/he.json index 7dbc50e246f..f67b83b243e 100644 --- a/homeassistant/components/mqtt/translations/he.json +++ b/homeassistant/components/mqtt/translations/he.json @@ -49,6 +49,12 @@ "button_triple_press": "\"{subtype}\" \u05dc\u05d7\u05d9\u05e6\u05d4 \u05de\u05e9\u05d5\u05dc\u05e9\u05ea" } }, + "issues": { + "deprecated_yaml": { + "description": "\u05de\u05d5\u05d2\u05d3\u05e8 \u05d1\u05d0\u05d5\u05e4\u05df \u05d9\u05d3\u05e0\u05d9 MQTT {platform} \u05e0\u05de\u05e6\u05d0 \u05ea\u05d7\u05ea \u05de\u05e4\u05ea\u05d7 \u05e4\u05dc\u05d8\u05e4\u05d5\u05e8\u05de\u05d4 `{platform}`.\n\n\u05d9\u05e9 \u05dc\u05d4\u05e2\u05d1\u05d9\u05e8 \u05d0\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05dc\u05de\u05e4\u05ea\u05d7 \u05d4\u05d0\u05d9\u05e0\u05d8\u05d2\u05e8\u05e6\u05d9\u05d4 `mqtt`\u05d5\u05dc\u05d4\u05e4\u05e2\u05d9\u05dc \u05de\u05d7\u05d3\u05e9 \u05d0\u05ea Home Assistant \u05db\u05d3\u05d9 \u05dc\u05e4\u05ea\u05d5\u05e8 \u05d1\u05e2\u05d9\u05d4 \u05d6\u05d5. \u05d9\u05e9 \u05dc\u05e2\u05d9\u05d9\u05df \u05d1[\u05ea\u05d9\u05e2\u05d5\u05d3]({more_info_url}), \u05dc\u05e7\u05d1\u05dc\u05ea \u05de\u05d9\u05d3\u05e2 \u05e0\u05d5\u05e1\u05e3.", + "title": "MQTT {platform} \u05d4\u05de\u05d5\u05d2\u05d3\u05e8 \u05d1\u05d0\u05d5\u05e4\u05df \u05d9\u05d3\u05e0\u05d9 \u05d6\u05e7\u05d5\u05e7 \u05dc\u05ea\u05e9\u05d5\u05de\u05ea \u05dc\u05d1" + } + }, "options": { "error": { "bad_birth": "\u05e0\u05d5\u05e9\u05d0 \u05dc\u05d9\u05d3\u05d4 \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9.", diff --git a/homeassistant/components/mqtt/translations/ja.json b/homeassistant/components/mqtt/translations/ja.json index 07af6230aa6..274e7666a30 100644 --- a/homeassistant/components/mqtt/translations/ja.json +++ b/homeassistant/components/mqtt/translations/ja.json @@ -49,6 +49,12 @@ "button_triple_press": "\"{subtype}\" 3\u56de\u30af\u30ea\u30c3\u30af" } }, + "issues": { + "deprecated_yaml": { + "description": "\u624b\u52d5\u3067\u69cb\u6210\u3055\u308c\u305f MQTT {platform} (s) \u306f\u3001\u30d7\u30e9\u30c3\u30c8\u30d5\u30a9\u30fc\u30e0 \u30ad\u30fc ` {platform} ` \u306e\u4e0b\u306b\u3042\u308a\u307e\u3059\u3002 \n\n\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001\u69cb\u6210\u3092\u300cmqtt\u300d\u7d71\u5408\u30ad\u30fc\u306b\u79fb\u52d5\u3057\u3001Home Assistant \u3092\u518d\u8d77\u52d5\u3057\u3066\u304f\u3060\u3055\u3044\u3002\u8a73\u7d30\u306b\u3064\u3044\u3066\u306f\u3001[\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8]( {more_info_url} ) \u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "title": "\u624b\u52d5\u3067\u8a2d\u5b9a\u3057\u305f\u3001MQTT {platform} \u306b\u306f\u6ce8\u610f\u304c\u5fc5\u8981\u3067\u3059" + } + }, "options": { "error": { "bad_birth": "(\u7121\u52b9\u306a)Invalid birth topic.", diff --git a/homeassistant/components/mqtt/translations/pl.json b/homeassistant/components/mqtt/translations/pl.json index 8b57c465af1..dad798296a1 100644 --- a/homeassistant/components/mqtt/translations/pl.json +++ b/homeassistant/components/mqtt/translations/pl.json @@ -49,6 +49,12 @@ "button_triple_press": "przycisk \"{subtype}\" zostanie trzykrotnie naci\u015bni\u0119ty" } }, + "issues": { + "deprecated_yaml": { + "description": "Znaleziono r\u0119cznie skonfigurowane platformy MQTT z kluczem `{platform}`.\n\nAby rozwi\u0105za\u0107 ten problem, przenie\u015b konfiguracj\u0119 do klucza integracji `mqtt` i uruchom ponownie Home Assistant. Zobacz [dokumentacj\u0119]( {more_info_url} ), aby uzyska\u0107 wi\u0119cej informacji.", + "title": "Twoja r\u0119cznie skonfigurowana platforma MQTT {platform} wymaga uwagi" + } + }, "options": { "error": { "bad_birth": "Nieprawid\u0142owy temat \"birth\"", diff --git a/homeassistant/components/mqtt/translations/sv.json b/homeassistant/components/mqtt/translations/sv.json index 46e3325f990..811da569992 100644 --- a/homeassistant/components/mqtt/translations/sv.json +++ b/homeassistant/components/mqtt/translations/sv.json @@ -49,6 +49,12 @@ "button_triple_press": "\" {subtype}\" trippelklickad" } }, + "issues": { + "deprecated_yaml": { + "description": "Manuellt konfigurerad MQTT {platform} (s) finns under plattformsnyckel ` {platform} `. \n\n Flytta konfigurationen till \"mqtt\"-integreringsnyckeln och starta om Home Assistant f\u00f6r att \u00e5tg\u00e4rda problemet. Se [dokumentationen]( {more_info_url} ), f\u00f6r mer information.", + "title": "Din manuellt konfigurerade MQTT {platform} (s) beh\u00f6ver \u00e5tg\u00e4rdas" + } + }, "options": { "error": { "bad_birth": "Ogiltigt f\u00f6delse\u00e4mne.", diff --git a/homeassistant/components/mqtt/translations/tr.json b/homeassistant/components/mqtt/translations/tr.json index d00aaf1c06b..eda134d8220 100644 --- a/homeassistant/components/mqtt/translations/tr.json +++ b/homeassistant/components/mqtt/translations/tr.json @@ -49,6 +49,12 @@ "button_triple_press": "\" {subtype} \" \u00fc\u00e7 kez t\u0131kland\u0131" } }, + "issues": { + "deprecated_yaml": { + "description": "El ile yap\u0131land\u0131r\u0131lm\u0131\u015f MQTT {platform} (lar) ` {platform} ` platform anahtar\u0131 alt\u0131nda bulundu. \n\n Bu sorunu gidermek i\u00e7in l\u00fctfen yap\u0131land\u0131rmay\u0131 `mqtt` entegrasyon anahtar\u0131na ta\u015f\u0131y\u0131n ve Home Assistant'\u0131 yeniden ba\u015flat\u0131n. Daha fazla bilgi i\u00e7in [belgelere]( {more_info_url} ) bak\u0131n.", + "title": "Manuel olarak yap\u0131land\u0131r\u0131lan MQTT {platform} (lar)\u0131n\u0131zla ilgilenilmesi gerekiyor" + } + }, "options": { "error": { "bad_birth": "Ge\u00e7ersiz do\u011fum konusu.", diff --git a/homeassistant/components/mqtt/util.py b/homeassistant/components/mqtt/util.py index 9ef30da7f3b..43734872e14 100644 --- a/homeassistant/components/mqtt/util.py +++ b/homeassistant/components/mqtt/util.py @@ -15,10 +15,12 @@ from .const import ( ATTR_QOS, ATTR_RETAIN, ATTR_TOPIC, + DATA_MQTT, DEFAULT_QOS, DEFAULT_RETAIN, DOMAIN, ) +from .models import MqttData def mqtt_config_entry_enabled(hass: HomeAssistant) -> bool | None: @@ -111,3 +113,10 @@ MQTT_WILL_BIRTH_SCHEMA = vol.Schema( }, required=True, ) + + +def get_mqtt_data(hass: HomeAssistant, ensure_exists: bool = False) -> MqttData: + """Return typed MqttData from hass.data[DATA_MQTT].""" + if ensure_exists: + return hass.data.setdefault(DATA_MQTT, MqttData()) + return hass.data[DATA_MQTT] diff --git a/homeassistant/components/mqtt_room/sensor.py b/homeassistant/components/mqtt_room/sensor.py index 276695d8edd..a6927048051 100644 --- a/homeassistant/components/mqtt_room/sensor.py +++ b/homeassistant/components/mqtt_room/sensor.py @@ -95,7 +95,7 @@ class MQTTRoomSensor(SensorEntity): self._distance = None self._updated = None - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Subscribe to MQTT events.""" @callback @@ -133,9 +133,7 @@ class MQTTRoomSensor(SensorEntity): ): update_state(**device) - return await mqtt.async_subscribe( - self.hass, self._state_topic, message_received, 1 - ) + await mqtt.async_subscribe(self.hass, self._state_topic, message_received, 1) @property def name(self): @@ -152,7 +150,7 @@ class MQTTRoomSensor(SensorEntity): """Return the current room of the entity.""" return self._state - def update(self): + def update(self) -> None: """Update the state for absent devices.""" if ( self._updated diff --git a/homeassistant/components/mvglive/sensor.py b/homeassistant/components/mvglive/sensor.py index d3405d0cce7..089d7ac5fbc 100644 --- a/homeassistant/components/mvglive/sensor.py +++ b/homeassistant/components/mvglive/sensor.py @@ -141,7 +141,7 @@ class MVGLiveSensor(SensorEntity): """Return the unit this state is expressed in.""" return TIME_MINUTES - def update(self): + def update(self) -> None: """Get the latest data and update the state.""" self.data.update() if not self.data.departures: diff --git a/homeassistant/components/my/manifest.json b/homeassistant/components/my/manifest.json index 8c88b092e1c..23d1b3d21e2 100644 --- a/homeassistant/components/my/manifest.json +++ b/homeassistant/components/my/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/my", "dependencies": ["frontend"], "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "system" } diff --git a/homeassistant/components/myq/translations/es.json b/homeassistant/components/myq/translations/es.json index 7cb7dcb9354..ccc709ce191 100644 --- a/homeassistant/components/myq/translations/es.json +++ b/homeassistant/components/myq/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "El servicio ya est\u00e1 configurado", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "error": { "cannot_connect": "No se pudo conectar", diff --git a/homeassistant/components/mysensors/climate.py b/homeassistant/components/mysensors/climate.py index 3e540bd5714..435bf2ffddb 100644 --- a/homeassistant/components/mysensors/climate.py +++ b/homeassistant/components/mysensors/climate.py @@ -3,10 +3,10 @@ from __future__ import annotations from typing import Any -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, + ClimateEntity, ClimateEntityFeature, HVACMode, ) diff --git a/homeassistant/components/mysensors/notify.py b/homeassistant/components/mysensors/notify.py index 43f4779604d..9083b325e8d 100644 --- a/homeassistant/components/mysensors/notify.py +++ b/homeassistant/components/mysensors/notify.py @@ -1,11 +1,12 @@ """MySensors notification service.""" from __future__ import annotations -from typing import Any +from typing import Any, cast from homeassistant.components.notify import ATTR_TARGET, BaseNotificationService from homeassistant.const import Platform from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .. import mysensors from .const import DevId, DiscoveryInfo @@ -13,15 +14,18 @@ from .const import DevId, DiscoveryInfo async def async_get_service( hass: HomeAssistant, - config: dict[str, Any], - discovery_info: DiscoveryInfo | None = None, + config: ConfigType, + discovery_info: DiscoveryInfoType | None = None, ) -> BaseNotificationService | None: """Get the MySensors notification service.""" if not discovery_info: return None new_devices = mysensors.setup_mysensors_platform( - hass, Platform.NOTIFY, discovery_info, MySensorsNotificationDevice + hass, + Platform.NOTIFY, + cast(DiscoveryInfo, discovery_info), + MySensorsNotificationDevice, ) if not new_devices: return None diff --git a/homeassistant/components/mysensors/sensor.py b/homeassistant/components/mysensors/sensor.py index 6f940c5d625..f21d343f9c3 100644 --- a/homeassistant/components/mysensors/sensor.py +++ b/homeassistant/components/mysensors/sensor.py @@ -94,12 +94,12 @@ SENSORS: dict[str, SensorEntityDescription] = { "V_WEIGHT": SensorEntityDescription( key="V_WEIGHT", native_unit_of_measurement=MASS_KILOGRAMS, - icon="mdi:weight-kilogram", + device_class=SensorDeviceClass.WEIGHT, ), "V_DISTANCE": SensorEntityDescription( key="V_DISTANCE", native_unit_of_measurement=LENGTH_METERS, - icon="mdi:ruler", + device_class=SensorDeviceClass.DISTANCE, ), "V_IMPEDANCE": SensorEntityDescription( key="V_IMPEDANCE", diff --git a/homeassistant/components/mysensors/translations/bg.json b/homeassistant/components/mysensors/translations/bg.json index 8abf7bbd1fa..f5422095553 100644 --- a/homeassistant/components/mysensors/translations/bg.json +++ b/homeassistant/components/mysensors/translations/bg.json @@ -4,6 +4,7 @@ "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", "invalid_port": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d \u043d\u043e\u043c\u0435\u0440 \u043d\u0430 \u043f\u043e\u0440\u0442", "invalid_serial": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d \u0441\u0435\u0440\u0438\u0435\u043d \u043f\u043e\u0440\u0442", + "mqtt_required": "MQTT \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u043d\u0435 \u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430", "port_out_of_range": "\u041d\u043e\u043c\u0435\u0440\u044a\u0442 \u043d\u0430 \u043f\u043e\u0440\u0442\u0430 \u0442\u0440\u044f\u0431\u0432\u0430 \u0434\u0430 \u0431\u044a\u0434\u0435 \u043d\u0430\u0439-\u043c\u0430\u043b\u043a\u043e 1 \u0438 \u043d\u0430\u0439-\u043c\u043d\u043e\u0433\u043e 65535", "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, @@ -27,6 +28,14 @@ "device": "IP \u0430\u0434\u0440\u0435\u0441 \u043d\u0430 \u0448\u043b\u044e\u0437\u0430", "tcp_port": "\u043f\u043e\u0440\u0442" } + }, + "select_gateway_type": { + "description": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u043a\u043e\u0439 \u0448\u043b\u044e\u0437 \u0434\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u0442\u0435.", + "menu_options": { + "gw_mqtt": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 MQTT \u0448\u043b\u044e\u0437", + "gw_serial": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0441\u0435\u0440\u0438\u0435\u043d \u0448\u043b\u044e\u0437", + "gw_tcp": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 TCP \u0448\u043b\u044e\u0437" + } } } } diff --git a/homeassistant/components/mysensors/translations/he.json b/homeassistant/components/mysensors/translations/he.json index bbe627fd878..9787081e32e 100644 --- a/homeassistant/components/mysensors/translations/he.json +++ b/homeassistant/components/mysensors/translations/he.json @@ -4,6 +4,7 @@ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "mqtt_required": "\u05e9\u05d9\u05dc\u05d5\u05d1 MQTT \u05d0\u05d9\u05e0\u05d5 \u05de\u05d5\u05d2\u05d3\u05e8", "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" }, "error": { @@ -28,6 +29,11 @@ "data": { "tcp_port": "\u05e4\u05ea\u05d7\u05d4" } + }, + "select_gateway_type": { + "menu_options": { + "gw_mqtt": "\u05e7\u05d1\u05d9\u05e2\u05ea \u05ea\u05e6\u05d5\u05e8\u05d4 \u05e9\u05dc \u05e9\u05e2\u05e8 MQTT" + } } } } diff --git a/homeassistant/components/mysensors/translations/nl.json b/homeassistant/components/mysensors/translations/nl.json index 8293e815d8c..44bd06022b9 100644 --- a/homeassistant/components/mysensors/translations/nl.json +++ b/homeassistant/components/mysensors/translations/nl.json @@ -14,6 +14,7 @@ "invalid_serial": "Ongeldige seri\u00eble poort", "invalid_subscribe_topic": "Ongeldig abonneeronderwerp", "invalid_version": "Ongeldige MySensors-versie", + "mqtt_required": "De MQTT-integratie is niet ingesteld", "not_a_number": "Voer een nummer in", "port_out_of_range": "Poortnummer moet minimaal 1 en maximaal 65535 zijn", "same_topic": "De topics abonneren en publiceren zijn hetzelfde", @@ -68,6 +69,12 @@ }, "description": "Ethernet gateway instellen" }, + "select_gateway_type": { + "menu_options": { + "gw_mqtt": "Configureer een MQTT-gateway", + "gw_serial": "Configureer een seri\u00eble gateway" + } + }, "user": { "data": { "gateway_type": "Gateway type" diff --git a/homeassistant/components/mysensors/translations/pt.json b/homeassistant/components/mysensors/translations/pt.json index 3ace45dd942..2eb65a87447 100644 --- a/homeassistant/components/mysensors/translations/pt.json +++ b/homeassistant/components/mysensors/translations/pt.json @@ -2,7 +2,18 @@ "config": { "abort": { "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "mqtt_required": "A integra\u00e7\u00e3o do MQTT n\u00e3o est\u00e1 configurada", "unknown": "Erro inesperado" + }, + "step": { + "select_gateway_type": { + "description": "Selecione qual gateway configurar.", + "menu_options": { + "gw_mqtt": "Configurar um gateway MQTT", + "gw_serial": "Configurar um gateway serial", + "gw_tcp": "Configurar um gateway TCP" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/mysensors/translations/sv.json b/homeassistant/components/mysensors/translations/sv.json index c41dc508691..7398b60346a 100644 --- a/homeassistant/components/mysensors/translations/sv.json +++ b/homeassistant/components/mysensors/translations/sv.json @@ -14,6 +14,7 @@ "invalid_serial": "Ogiltig serieport", "invalid_subscribe_topic": "Ogiltigt \u00e4mne f\u00f6r prenumeration", "invalid_version": "Ogiltig version av MySensors", + "mqtt_required": "MQTT-integrationen \u00e4r inte konfigurerad", "not_a_number": "Ange ett nummer", "port_out_of_range": "Portnummer m\u00e5ste vara minst 1 och h\u00f6gst 65535", "same_topic": "\u00c4mnen f\u00f6r prenumeration och publicering \u00e4r desamma", @@ -68,6 +69,14 @@ }, "description": "Ethernet-gateway-inst\u00e4llning" }, + "select_gateway_type": { + "description": "V\u00e4lj vilken gateway som ska konfigureras.", + "menu_options": { + "gw_mqtt": "Konfigurera en MQTT-gateway", + "gw_serial": "Konfigurera en seriell gateway", + "gw_tcp": "Konfigurera en TCP-gateway" + } + }, "user": { "data": { "gateway_type": "Gateway typ" diff --git a/homeassistant/components/mystrom/switch.py b/homeassistant/components/mystrom/switch.py index 41303b25a9f..7bce3000424 100644 --- a/homeassistant/components/mystrom/switch.py +++ b/homeassistant/components/mystrom/switch.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any from pymystrom.exceptions import MyStromConnectionError from pymystrom.switch import MyStromSwitch as _MyStromSwitch @@ -77,21 +78,21 @@ class MyStromSwitch(SwitchEntity): """Could the device be accessed during the last update call.""" return self._available - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" try: await self.plug.turn_on() except MyStromConnectionError: _LOGGER.error("No route to myStrom plug") - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the switch off.""" try: await self.plug.turn_off() except MyStromConnectionError: _LOGGER.error("No route to myStrom plug") - async def async_update(self): + async def async_update(self) -> None: """Get the latest data from the device and update the data.""" try: await self.plug.get_state() diff --git a/homeassistant/components/nad/media_player.py b/homeassistant/components/nad/media_player.py index f031175a321..531dabfde70 100644 --- a/homeassistant/components/nad/media_player.py +++ b/homeassistant/components/nad/media_player.py @@ -8,15 +8,9 @@ from homeassistant.components.media_player import ( PLATFORM_SCHEMA, MediaPlayerEntity, MediaPlayerEntityFeature, + MediaPlayerState, ) -from homeassistant.const import ( - CONF_HOST, - CONF_NAME, - CONF_PORT, - CONF_TYPE, - STATE_OFF, - STATE_ON, -) +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, CONF_TYPE from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -133,34 +127,34 @@ class NAD(MediaPlayerEntity): """Boolean if volume is currently muted.""" return self._mute - def turn_off(self): + def turn_off(self) -> None: """Turn the media player off.""" self._nad_receiver.main_power("=", "Off") - def turn_on(self): + def turn_on(self) -> None: """Turn the media player on.""" self._nad_receiver.main_power("=", "On") - def volume_up(self): + def volume_up(self) -> None: """Volume up the media player.""" self._nad_receiver.main_volume("+") - def volume_down(self): + def volume_down(self) -> None: """Volume down the media player.""" self._nad_receiver.main_volume("-") - def set_volume_level(self, volume): + def set_volume_level(self, volume: float) -> None: """Set volume level, range 0..1.""" self._nad_receiver.main_volume("=", self.calc_db(volume)) - def mute_volume(self, mute): + def mute_volume(self, mute: bool) -> None: """Mute (true) or unmute (false) media player.""" if mute: self._nad_receiver.main_mute("=", "On") else: self._nad_receiver.main_mute("=", "Off") - def select_source(self, source): + def select_source(self, source: str) -> None: """Select input source.""" self._nad_receiver.main_source("=", self._reverse_mapping.get(source)) @@ -175,7 +169,7 @@ class NAD(MediaPlayerEntity): return sorted(self._reverse_mapping) @property - def available(self): + def available(self) -> bool: """Return if device is available.""" return self._state is not None @@ -186,10 +180,12 @@ class NAD(MediaPlayerEntity): self._state = None return self._state = ( - STATE_ON if self._nad_receiver.main_power("?") == "On" else STATE_OFF + MediaPlayerState.ON + if self._nad_receiver.main_power("?") == "On" + else MediaPlayerState.OFF ) - if self._state == STATE_ON: + if self._state == MediaPlayerState.ON: self._mute = self._nad_receiver.main_mute("?") == "On" volume = self._nad_receiver.main_volume("?") # Some receivers cannot report the volume, e.g. C 356BEE, @@ -257,37 +253,37 @@ class NADtcp(MediaPlayerEntity): """Boolean if volume is currently muted.""" return self._mute - def turn_off(self): + def turn_off(self) -> None: """Turn the media player off.""" self._nad_receiver.power_off() - def turn_on(self): + def turn_on(self) -> None: """Turn the media player on.""" self._nad_receiver.power_on() - def volume_up(self): + def volume_up(self) -> None: """Step volume up in the configured increments.""" self._nad_receiver.set_volume(self._nad_volume + 2 * self._volume_step) - def volume_down(self): + def volume_down(self) -> None: """Step volume down in the configured increments.""" self._nad_receiver.set_volume(self._nad_volume - 2 * self._volume_step) - def set_volume_level(self, volume): + def set_volume_level(self, volume: float) -> None: """Set volume level, range 0..1.""" nad_volume_to_set = int( round(volume * (self._max_vol - self._min_vol) + self._min_vol) ) self._nad_receiver.set_volume(nad_volume_to_set) - def mute_volume(self, mute): + def mute_volume(self, mute: bool) -> None: """Mute (true) or unmute (false) media player.""" if mute: self._nad_receiver.mute() else: self._nad_receiver.unmute() - def select_source(self, source): + def select_source(self, source: str) -> None: """Select input source.""" self._nad_receiver.select_source(source) @@ -301,7 +297,7 @@ class NADtcp(MediaPlayerEntity): """List of available input sources.""" return self._nad_receiver.available_sources() - def update(self): + def update(self) -> None: """Get the latest details from the device.""" try: nad_status = self._nad_receiver.status() @@ -312,9 +308,9 @@ class NADtcp(MediaPlayerEntity): # Update on/off state if nad_status["power"]: - self._state = STATE_ON + self._state = MediaPlayerState.ON else: - self._state = STATE_OFF + self._state = MediaPlayerState.OFF # Update current volume self._volume = self.nad_vol_to_internal_vol(nad_status["volume"]) diff --git a/homeassistant/components/nam/translations/cs.json b/homeassistant/components/nam/translations/cs.json index 1b979ab1412..01c1215497b 100644 --- a/homeassistant/components/nam/translations/cs.json +++ b/homeassistant/components/nam/translations/cs.json @@ -5,8 +5,28 @@ "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" }, "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "credentials": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + }, + "reauth_confirm": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + }, + "user": { + "data": { + "host": "Hostitel" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/nam/translations/es.json b/homeassistant/components/nam/translations/es.json index c0ac0958d24..7b9558537a6 100644 --- a/homeassistant/components/nam/translations/es.json +++ b/homeassistant/components/nam/translations/es.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", "device_unsupported": "El dispositivo no es compatible.", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", + "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." }, "error": { @@ -28,7 +28,7 @@ "password": "Contrase\u00f1a", "username": "Nombre de usuario" }, - "description": "Por favor, introduce el nombre de usuario y la contrase\u00f1a correctos para el host: {host}" + "description": "Por favor, introduce el nombre de usuario y contrase\u00f1a correctos para el host: {host}" }, "user": { "data": { diff --git a/homeassistant/components/ambee/translations/sensor.bg.json b/homeassistant/components/nam/translations/sensor.bg.json similarity index 50% rename from homeassistant/components/ambee/translations/sensor.bg.json rename to homeassistant/components/nam/translations/sensor.bg.json index 07977ca4abf..5c772d85ca1 100644 --- a/homeassistant/components/ambee/translations/sensor.bg.json +++ b/homeassistant/components/nam/translations/sensor.bg.json @@ -1,10 +1,11 @@ { "state": { - "ambee__risk": { + "nam__caqi_level": { "high": "\u0412\u0438\u0441\u043e\u043a\u043e", "low": "\u041d\u0438\u0441\u043a\u043e", - "moderate": "\u0423\u043c\u0435\u0440\u0435\u043d\u043e", - "very high": "\u041c\u043d\u043e\u0433\u043e \u0432\u0438\u0441\u043e\u043a\u043e" + "medium": "\u0421\u0440\u0435\u0434\u043d\u043e", + "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/sensor.ca.json b/homeassistant/components/nam/translations/sensor.ca.json new file mode 100644 index 00000000000..ec1a642cbd2 --- /dev/null +++ b/homeassistant/components/nam/translations/sensor.ca.json @@ -0,0 +1,11 @@ +{ + "state": { + "nam__caqi_level": { + "high": "Alt", + "low": "Baix", + "medium": "Mitj\u00e0", + "very high": "Molt alt", + "very low": "Molt baix" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nam/translations/sensor.hu.json b/homeassistant/components/nam/translations/sensor.hu.json new file mode 100644 index 00000000000..ee30c2e4c44 --- /dev/null +++ b/homeassistant/components/nam/translations/sensor.hu.json @@ -0,0 +1,11 @@ +{ + "state": { + "nam__caqi_level": { + "high": "Magas", + "low": "Alacsony", + "medium": "K\u00f6zepes", + "very high": "Nagyon magas", + "very low": "Nagyon alacsony" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nam/translations/sensor.id.json b/homeassistant/components/nam/translations/sensor.id.json new file mode 100644 index 00000000000..6b208a54362 --- /dev/null +++ b/homeassistant/components/nam/translations/sensor.id.json @@ -0,0 +1,11 @@ +{ + "state": { + "nam__caqi_level": { + "high": "Tinggi", + "low": "Rendah", + "medium": "Sedang", + "very high": "Sangat tinggi", + "very low": "Sangat rendah" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nam/translations/sensor.ja.json b/homeassistant/components/nam/translations/sensor.ja.json new file mode 100644 index 00000000000..86356081b8a --- /dev/null +++ b/homeassistant/components/nam/translations/sensor.ja.json @@ -0,0 +1,11 @@ +{ + "state": { + "nam__caqi_level": { + "high": "\u9ad8", + "low": "\u4f4e", + "medium": "\u4e2d", + "very high": "\u975e\u5e38\u306b\u9ad8\u3044", + "very low": "\u3068\u3066\u3082\u4f4e\u3044" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nam/translations/sensor.nl.json b/homeassistant/components/nam/translations/sensor.nl.json new file mode 100644 index 00000000000..b607f922b98 --- /dev/null +++ b/homeassistant/components/nam/translations/sensor.nl.json @@ -0,0 +1,11 @@ +{ + "state": { + "nam__caqi_level": { + "high": "Hoog", + "low": "Laag", + "medium": "Medium", + "very high": "Heel hoog", + "very low": "Heel laag" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nam/translations/sensor.pt.json b/homeassistant/components/nam/translations/sensor.pt.json new file mode 100644 index 00000000000..a46b60abec3 --- /dev/null +++ b/homeassistant/components/nam/translations/sensor.pt.json @@ -0,0 +1,7 @@ +{ + "state": { + "nam__caqi_level": { + "high": "Alto" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nam/translations/sensor.sv.json b/homeassistant/components/nam/translations/sensor.sv.json new file mode 100644 index 00000000000..5039ab0d5ee --- /dev/null +++ b/homeassistant/components/nam/translations/sensor.sv.json @@ -0,0 +1,11 @@ +{ + "state": { + "nam__caqi_level": { + "high": "H\u00f6g", + "low": "L\u00e5g", + "medium": "Medium", + "very high": "V\u00e4ldigt h\u00f6gt", + "very low": "Mycket l\u00e5g" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nam/translations/sensor.tr.json b/homeassistant/components/nam/translations/sensor.tr.json new file mode 100644 index 00000000000..d271db62efe --- /dev/null +++ b/homeassistant/components/nam/translations/sensor.tr.json @@ -0,0 +1,11 @@ +{ + "state": { + "nam__caqi_level": { + "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/nanoleaf/translations/es.json b/homeassistant/components/nanoleaf/translations/es.json index 9de6598da5c..f8a43b4fece 100644 --- a/homeassistant/components/nanoleaf/translations/es.json +++ b/homeassistant/components/nanoleaf/translations/es.json @@ -4,7 +4,7 @@ "already_configured": "El dispositivo ya est\u00e1 configurado", "cannot_connect": "No se pudo conectar", "invalid_token": "Token de acceso no v\u00e1lido", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente", "unknown": "Error inesperado" }, "error": { diff --git a/homeassistant/components/neato/translations/es.json b/homeassistant/components/neato/translations/es.json index 4dcab62da43..b9e818eda5e 100644 --- a/homeassistant/components/neato/translations/es.json +++ b/homeassistant/components/neato/translations/es.json @@ -5,7 +5,7 @@ "authorize_url_timeout": "Se agot\u00f3 el tiempo de espera para generar la URL de autorizaci\u00f3n.", "missing_configuration": "El componente no est\u00e1 configurado. Por favor, sigue la documentaci\u00f3n.", "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [revisa la secci\u00f3n de ayuda]({docs_url})", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "create_entry": { "default": "Autenticado correctamente" diff --git a/homeassistant/components/nederlandse_spoorwegen/sensor.py b/homeassistant/components/nederlandse_spoorwegen/sensor.py index 84fd1f0569b..063fd12f5e0 100644 --- a/homeassistant/components/nederlandse_spoorwegen/sensor.py +++ b/homeassistant/components/nederlandse_spoorwegen/sensor.py @@ -219,7 +219,7 @@ class NSDepartureSensor(SensorEntity): return attributes @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): + def update(self) -> None: """Get the trip information.""" # If looking for a specific trip time, update around that trip time only. diff --git a/homeassistant/components/ness_alarm/binary_sensor.py b/homeassistant/components/ness_alarm/binary_sensor.py index 4855ce28b72..117d65b0940 100644 --- a/homeassistant/components/ness_alarm/binary_sensor.py +++ b/homeassistant/components/ness_alarm/binary_sensor.py @@ -55,7 +55,7 @@ class NessZoneBinarySensor(BinarySensorEntity): self._type = zone_type self._state = 0 - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register callbacks.""" self.async_on_remove( async_dispatcher_connect( diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index aa50b999c1c..99760d4be39 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -27,7 +27,7 @@ from homeassistant.components.application_credentials import ( async_import_client_credential, ) from homeassistant.components.camera import Image, img_util -from homeassistant.components.http.const import KEY_HASS_USER +from homeassistant.components.http import KEY_HASS_USER from homeassistant.components.http.view import HomeAssistantView from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( diff --git a/homeassistant/components/nest/camera_sdm.py b/homeassistant/components/nest/camera_sdm.py index e148916d7e8..f83f914385e 100644 --- a/homeassistant/components/nest/camera_sdm.py +++ b/homeassistant/components/nest/camera_sdm.py @@ -18,8 +18,7 @@ from google_nest_sdm.device import Device from google_nest_sdm.device_manager import DeviceManager from google_nest_sdm.exceptions import ApiException -from homeassistant.components.camera import Camera, CameraEntityFeature -from homeassistant.components.camera.const import StreamType +from homeassistant.components.camera import Camera, CameraEntityFeature, StreamType from homeassistant.components.stream import CONF_EXTRA_PART_WAIT_TIME from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant diff --git a/homeassistant/components/nest/climate_sdm.py b/homeassistant/components/nest/climate_sdm.py index 87cbf2331f4..dd257bb9301 100644 --- a/homeassistant/components/nest/climate_sdm.py +++ b/homeassistant/components/nest/climate_sdm.py @@ -15,8 +15,7 @@ from google_nest_sdm.thermostat_traits import ( ThermostatTemperatureSetpointTrait, ) -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( ATTR_HVAC_MODE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, @@ -24,6 +23,7 @@ from homeassistant.components.climate.const import ( FAN_ON, PRESET_ECO, PRESET_NONE, + ClimateEntity, ClimateEntityFeature, HVACAction, HVACMode, @@ -315,6 +315,8 @@ class ThermostatEntity(ClimateEntity): """Set new target preset mode.""" if preset_mode not in self.preset_modes: raise ValueError(f"Unsupported preset_mode '{preset_mode}'") + if self.preset_mode == preset_mode: # API doesn't like duplicate preset modes + return trait = self._device.traits[ThermostatEcoTrait.NAME] try: await trait.set_mode(PRESET_INV_MODE_MAP[preset_mode]) diff --git a/homeassistant/components/nest/legacy/climate.py b/homeassistant/components/nest/legacy/climate.py index 3a735fe44c3..13728585e39 100644 --- a/homeassistant/components/nest/legacy/climate.py +++ b/homeassistant/components/nest/legacy/climate.py @@ -6,15 +6,16 @@ import logging from nest.nest import APIError import voluptuous as vol -from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, FAN_AUTO, FAN_ON, + PLATFORM_SCHEMA, PRESET_AWAY, PRESET_ECO, PRESET_NONE, + ClimateEntity, ClimateEntityFeature, HVACAction, HVACMode, diff --git a/homeassistant/components/nest/manifest.json b/homeassistant/components/nest/manifest.json index 72e0aed8420..90fad5cf185 100644 --- a/homeassistant/components/nest/manifest.json +++ b/homeassistant/components/nest/manifest.json @@ -1,6 +1,6 @@ { "domain": "nest", - "name": "Nest", + "name": "Google Nest", "config_flow": true, "dependencies": ["ffmpeg", "http", "application_credentials"], "after_dependencies": ["media_source"], diff --git a/homeassistant/components/nest/media_source.py b/homeassistant/components/nest/media_source.py index 7b5b96b7145..d9478a99316 100644 --- a/homeassistant/components/nest/media_source.py +++ b/homeassistant/components/nest/media_source.py @@ -36,14 +36,7 @@ from google_nest_sdm.google_nest_subscriber import GoogleNestSubscriber from google_nest_sdm.transcoder import Transcoder from homeassistant.components.ffmpeg import get_ffmpeg_manager -from homeassistant.components.media_player.const import ( - MEDIA_CLASS_DIRECTORY, - MEDIA_CLASS_IMAGE, - MEDIA_CLASS_VIDEO, - MEDIA_TYPE_IMAGE, - MEDIA_TYPE_VIDEO, -) -from homeassistant.components.media_player.errors import BrowseError +from homeassistant.components.media_player import BrowseError, MediaClass, MediaType from homeassistant.components.media_source.error import Unresolvable from homeassistant.components.media_source.models import ( BrowseMediaSource, @@ -450,9 +443,9 @@ def _browse_root() -> BrowseMediaSource: return BrowseMediaSource( domain=DOMAIN, identifier="", - media_class=MEDIA_CLASS_DIRECTORY, - media_content_type=MEDIA_TYPE_VIDEO, - children_media_class=MEDIA_CLASS_VIDEO, + media_class=MediaClass.DIRECTORY, + media_content_type=MediaType.VIDEO, + children_media_class=MediaClass.VIDEO, title=MEDIA_SOURCE_TITLE, can_play=False, can_expand=True, @@ -482,9 +475,9 @@ def _browse_device(device_id: MediaId, device: Device) -> BrowseMediaSource: return BrowseMediaSource( domain=DOMAIN, identifier=device_id.identifier, - media_class=MEDIA_CLASS_DIRECTORY, - media_content_type=MEDIA_TYPE_VIDEO, - children_media_class=MEDIA_CLASS_VIDEO, + media_class=MediaClass.DIRECTORY, + media_content_type=MediaType.VIDEO, + children_media_class=MediaClass.VIDEO, title=DEVICE_TITLE_FORMAT.format(device_name=device_info.device_name), can_play=False, can_expand=True, @@ -503,8 +496,8 @@ def _browse_clip_preview( return BrowseMediaSource( domain=DOMAIN, identifier=event_id.identifier, - media_class=MEDIA_CLASS_IMAGE, - media_content_type=MEDIA_TYPE_IMAGE, + media_class=MediaClass.IMAGE, + media_content_type=MediaType.IMAGE, title=CLIP_TITLE_FORMAT.format( event_name=", ".join(types), event_time=dt_util.as_local(event.timestamp).strftime(DATE_STR_FORMAT), @@ -525,8 +518,8 @@ def _browse_image_event( return BrowseMediaSource( domain=DOMAIN, identifier=event_id.identifier, - media_class=MEDIA_CLASS_IMAGE, - media_content_type=MEDIA_TYPE_IMAGE, + media_class=MediaClass.IMAGE, + media_content_type=MediaType.IMAGE, title=CLIP_TITLE_FORMAT.format( event_name=MEDIA_SOURCE_EVENT_TITLE_MAP.get(event.event_type, "Event"), event_time=dt_util.as_local(event.timestamp).strftime(DATE_STR_FORMAT), diff --git a/homeassistant/components/nest/translations/bg.json b/homeassistant/components/nest/translations/bg.json index efa1378b81e..41f20ca4a5f 100644 --- a/homeassistant/components/nest/translations/bg.json +++ b/homeassistant/components/nest/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", "authorize_url_timeout": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0433\u0435\u043d\u0435\u0440\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0430\u0434\u0440\u0435\u0441 \u0437\u0430 \u043e\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f \u0432 \u0441\u0440\u043e\u043a." }, "create_entry": { @@ -30,5 +31,10 @@ "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u043d\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430" } } + }, + "issues": { + "deprecated_yaml": { + "title": "YAML \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u043d\u0430 Nest \u0441\u0435 \u043f\u0440\u0435\u043c\u0430\u0445\u0432\u0430" + } } } \ No newline at end of file diff --git a/homeassistant/components/nest/translations/cs.json b/homeassistant/components/nest/translations/cs.json index a0fe869cd36..b41ce556cfa 100644 --- a/homeassistant/components/nest/translations/cs.json +++ b/homeassistant/components/nest/translations/cs.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "\u00da\u010det je ji\u017e nastaven", "authorize_url_timeout": "\u010casov\u00fd limit autoriza\u010dn\u00edho URL vypr\u0161el", "missing_configuration": "Komponenta nen\u00ed nastavena. Postupujte podle dokumentace.", "no_url_available": "Nen\u00ed k dispozici \u017e\u00e1dn\u00e1 adresa URL. Informace o t\u00e9to chyb\u011b naleznete [v sekci n\u00e1pov\u011bdy]({docs_url})", diff --git a/homeassistant/components/nest/translations/es.json b/homeassistant/components/nest/translations/es.json index 7c7c8444479..3cd92fd644d 100644 --- a/homeassistant/components/nest/translations/es.json +++ b/homeassistant/components/nest/translations/es.json @@ -9,7 +9,7 @@ "invalid_access_token": "Token de acceso no v\u00e1lido", "missing_configuration": "El componente no est\u00e1 configurado. Por favor, sigue la documentaci\u00f3n.", "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [revisa la secci\u00f3n de ayuda]({docs_url})", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente", "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n.", "unknown_authorize_url_generation": "Error desconocido al generar una URL de autorizaci\u00f3n." }, diff --git a/homeassistant/components/nest/translations/fr.json b/homeassistant/components/nest/translations/fr.json index 7cbed195e0c..4d7f2d35f06 100644 --- a/homeassistant/components/nest/translations/fr.json +++ b/homeassistant/components/nest/translations/fr.json @@ -91,7 +91,7 @@ }, "issues": { "deprecated_yaml": { - "title": "La configuration YAML pour Nest est en cours de suppression" + "title": "La configuration YAML pour Nest sera bient\u00f4t supprim\u00e9e" } } } \ No newline at end of file diff --git a/homeassistant/components/nest/translations/id.json b/homeassistant/components/nest/translations/id.json index 1d69f9f48bd..d9ed2039800 100644 --- a/homeassistant/components/nest/translations/id.json +++ b/homeassistant/components/nest/translations/id.json @@ -1,6 +1,6 @@ { "application_credentials": { - "description": "Ikuti [petunjuk]({more_info_url}) untuk mengonfigurasi Konsol Cloud:\n\n1. Buka [Layar persetujuan OAuth]({oauth_consent_url}) dan konfigurasikan\n1. Buka [Kredentsial]({oauth_creds_url}) dan klik **Buat Kredensial**.\n1. Dari daftar pilihan, pilih **ID klien OAuth**.\n1. Pilih **Aplikasi Web** untuk Jenis Aplikasi.\n1. Tambahkan '{redirect_url}' di bawah *URI pengarahan ulang yand diotorisasi*." + "description": "Ikuti [petunjuk]({more_info_url}) untuk mengonfigurasi Konsol Cloud:\n\n1. Buka [Layar persetujuan OAuth]({oauth_consent_url}) dan konfigurasikan\n1. Buka [Kredentsial]({oauth_creds_url}) dan klik **Buat Kredensial**.\n1. Dari daftar pilihan, pilih **ID klien OAuth**.\n1. Pilih **Aplikasi Web** untuk Jenis Aplikasi.\n1. Tambahkan `{redirect_url}` di bawah *URI pengarahan ulang yand diotorisasi*." }, "config": { "abort": { diff --git a/homeassistant/components/nest/translations/ja.json b/homeassistant/components/nest/translations/ja.json index 06c5e54ef88..68085a7e30a 100644 --- a/homeassistant/components/nest/translations/ja.json +++ b/homeassistant/components/nest/translations/ja.json @@ -1,4 +1,7 @@ { + "application_credentials": { + "description": "[\u624b\u9806]( {more_info_url} ) \u306b\u5f93\u3063\u3066\u3001Cloud Console \u3092\u69cb\u6210\u3057\u307e\u3059\u3002 \n\n 1. [OAuth\u540c\u610f\u753b\u9762]( {oauth_consent_url} )\u306b\u79fb\u52d5\u3057\u3066\u8a2d\u5b9a\n1. [Credentials]( {oauth_creds_url} ) \u306b\u79fb\u52d5\u3057\u3001**Create Credentials** \u3092\u30af\u30ea\u30c3\u30af\u3057\u307e\u3059\u3002\n 1. \u30c9\u30ed\u30c3\u30d7\u30c0\u30a6\u30f3 \u30ea\u30b9\u30c8\u304b\u3089 **OAuth \u30af\u30e9\u30a4\u30a2\u30f3\u30c8 ID** \u3092\u9078\u629e\u3057\u307e\u3059\u3002\n 1. [\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u306e\u7a2e\u985e] \u3067 [**Web \u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3**] \u3092\u9078\u629e\u3057\u307e\u3059\u3002\n 1. *Authorized redirect URI* \u306e\u4e0b\u306b ` {redirect_url} ` \u3092\u8ffd\u52a0\u3057\u307e\u3059\u3002" + }, "config": { "abort": { "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", @@ -31,6 +34,7 @@ "title": "Google\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u30ea\u30f3\u30af\u3059\u308b" }, "auth_upgrade": { + "description": "App Auth\u306f\u3001\u30bb\u30ad\u30e5\u30ea\u30c6\u30a3\u3092\u5411\u4e0a\u3055\u305b\u308b\u305f\u3081\u306bGoogle\u306b\u3088\u3063\u3066\u5ec3\u6b62\u3055\u308c\u307e\u3057\u305f\u3002\u65b0\u3057\u3044\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u306e\u8cc7\u683c\u60c5\u5831\u3092\u4f5c\u6210\u3059\u308b\u3053\u3068\u304c\u5fc5\u8981\u3067\u3059\u3002 \n\n [\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8]({more_info_url}) \u3092\u958b\u304d\u3001\u6b21\u306e\u624b\u9806\u306b\u5f93\u3063\u3066\u3001Nest\u30c7\u30d0\u30a4\u30b9\u3078\u306e\u30a2\u30af\u30bb\u30b9\u3092\u5fa9\u5143\u3059\u308b\u305f\u3081\u306b\u5fc5\u8981\u306a\u624b\u9806\u3092\u8aac\u660e\u3057\u307e\u3059\u3002", "title": "\u30cd\u30b9\u30c8: \u30a2\u30d7\u30ea\u8a8d\u8a3c\u306e\u975e\u63a8\u5968" }, "cloud_project": { @@ -41,15 +45,18 @@ "title": "\u30cd\u30b9\u30c8: \u30af\u30e9\u30a6\u30c9\u30d7\u30ed\u30b8\u30a7\u30af\u30c8ID\u3092\u5165\u529b" }, "create_cloud_project": { + "description": "Nest \u7d71\u5408\u306b\u3088\u308a\u3001Smart Device Management API \u3092\u4f7f\u7528\u3057\u3066\u3001Nest \u30b5\u30fc\u30e2\u30b9\u30bf\u30c3\u30c8\u3001\u30ab\u30e1\u30e9\u3001\u304a\u3088\u3073\u30c9\u30a2\u30d9\u30eb\u3092\u7d71\u5408\u3067\u304d\u307e\u3059\u3002 SDM API \u306b\u306f **5 \u7c73\u30c9\u30eb**\u306e 1 \u56de\u9650\u308a\u306e\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u6599\u91d1\u304c\u5fc5\u8981\u3067\u3059\u3002 [\u8a73\u7d30]( {more_info_url} ) \u306e\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002 \n\n 1. [Google Cloud Console]( {cloud_console_url} ) \u306b\u79fb\u52d5\u3057\u307e\u3059\u3002\n 1. \u521d\u3081\u3066\u306e\u30d7\u30ed\u30b8\u30a7\u30af\u30c8\u306e\u5834\u5408\u306f\u3001**Create Project** \u3092\u30af\u30ea\u30c3\u30af\u3057\u3066\u304b\u3089 **New Project** \u3092\u30af\u30ea\u30c3\u30af\u3057\u307e\u3059\u3002\n 1. \u30af\u30e9\u30a6\u30c9 \u30d7\u30ed\u30b8\u30a7\u30af\u30c8\u306b\u540d\u524d\u3092\u4ed8\u3051\u3066\u3001[**\u4f5c\u6210**] \u3092\u30af\u30ea\u30c3\u30af\u3057\u307e\u3059\u3002\n 1. \u5f8c\u3067\u5fc5\u8981\u306b\u306a\u308b\u305f\u3081\u3001\u30af\u30e9\u30a6\u30c9 \u30d7\u30ed\u30b8\u30a7\u30af\u30c8 ID \u3092\u4fdd\u5b58\u3057\u307e\u3059 (\u4f8b: *example-project-12345*)\u3002\n 1. [Smart Device Management API]( {sdm_api_url} ) \u306e API \u30e9\u30a4\u30d6\u30e9\u30ea\u306b\u79fb\u52d5\u3057\u3001[**\u6709\u52b9\u306b\u3059\u308b**] \u3092\u30af\u30ea\u30c3\u30af\u3057\u307e\u3059\u3002\n 1. [Cloud Pub/Sub API]( {pubsub_api_url} ) \u306e API \u30e9\u30a4\u30d6\u30e9\u30ea\u306b\u79fb\u52d5\u3057\u3001[**\u6709\u52b9\u306b\u3059\u308b**] \u3092\u30af\u30ea\u30c3\u30af\u3057\u307e\u3059\u3002 \n\n\u30af\u30e9\u30a6\u30c9 \u30d7\u30ed\u30b8\u30a7\u30af\u30c8\u304c\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3055\u308c\u305f\u3089\u3001\u6b21\u306b\u9032\u307f\u307e\u3059\u3002", "title": "\u30cd\u30b9\u30c8: \u30af\u30e9\u30a6\u30c9\u30d7\u30ed\u30b8\u30a7\u30af\u30c8\u306e\u4f5c\u6210\u3068\u8a2d\u5b9a" }, "device_project": { "data": { "project_id": "\u30c7\u30d0\u30a4\u30b9\u30a2\u30af\u30bb\u30b9\u30d7\u30ed\u30b8\u30a7\u30af\u30c8ID" }, + "description": "\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u306b **5 \u7c73\u30c9\u30eb\u306e\u624b\u6570\u6599\u304c\u5fc5\u8981**\u306a Nest Device Access \u30d7\u30ed\u30b8\u30a7\u30af\u30c8\u3092\u4f5c\u6210\u3057\u307e\u3059\u3002\n 1. [\u30c7\u30d0\u30a4\u30b9 \u30a2\u30af\u30bb\u30b9 \u30b3\u30f3\u30bd\u30fc\u30eb]( {device_access_console_url} ) \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3001\u652f\u6255\u3044\u30d5\u30ed\u30fc\u3092\u5b9f\u884c\u3057\u307e\u3059\u3002\n 1. **\u30d7\u30ed\u30b8\u30a7\u30af\u30c8\u306e\u4f5c\u6210**\u3092\u30af\u30ea\u30c3\u30af\u3057\u307e\u3059\n1. \u30c7\u30d0\u30a4\u30b9 \u30a2\u30af\u30bb\u30b9 \u30d7\u30ed\u30b8\u30a7\u30af\u30c8\u306b\u540d\u524d\u3092\u4ed8\u3051\u3066\u3001**\u6b21\u3078**\u3092\u30af\u30ea\u30c3\u30af\u3057\u307e\u3059\u3002\n 1. OAuth \u30af\u30e9\u30a4\u30a2\u30f3\u30c8 ID \u3092\u5165\u529b\u3057\u307e\u3059\n1. **\u6709\u52b9\u5316**\u304a\u3088\u3073**\u30d7\u30ed\u30b8\u30a7\u30af\u30c8\u306e\u4f5c\u6210**\u3092\u30af\u30ea\u30c3\u30af\u3057\u3066\u3001\u30a4\u30d9\u30f3\u30c8\u3092\u6709\u52b9\u306b\u3057\u307e\u3059\u3002 \n\n\u4ee5\u4e0b\u306b\u30c7\u30d0\u30a4\u30b9 \u30a2\u30af\u30bb\u30b9 \u30d7\u30ed\u30b8\u30a7\u30af\u30c8 ID \u3092\u5165\u529b\u3057\u307e\u3059 ([\u8a73\u7d30]( {more_info_url} ))\u3002\n", "title": "\u30cd\u30b9\u30c8: \u30c7\u30d0\u30a4\u30b9\u30a2\u30af\u30bb\u30b9\u30d7\u30ed\u30b8\u30a7\u30af\u30c8\u306e\u4f5c\u6210" }, "device_project_upgrade": { + "description": "Nest Device Access Project \u3092\u65b0\u3057\u3044 OAuth \u30af\u30e9\u30a4\u30a2\u30f3\u30c8 ID \u3067\u66f4\u65b0\u3057\u307e\u3059 ([\u8a73\u7d30]( {more_info_url} ))\n 1. [\u30c7\u30d0\u30a4\u30b9 \u30a2\u30af\u30bb\u30b9 \u30b3\u30f3\u30bd\u30fc\u30eb]( {device_access_console_url} ) \u306b\u79fb\u52d5\u3057\u307e\u3059\u3002\n 1. *OAuth \u30af\u30e9\u30a4\u30a2\u30f3\u30c8 ID* \u306e\u6a2a\u306b\u3042\u308b\u3054\u307f\u7bb1\u30a2\u30a4\u30b3\u30f3\u3092\u30af\u30ea\u30c3\u30af\u3057\u307e\u3059\u3002\n 1. [...] \u30aa\u30fc\u30d0\u30fc\u30d5\u30ed\u30fc \u30e1\u30cb\u30e5\u30fc\u3092\u30af\u30ea\u30c3\u30af\u3057\u3001[\u30af\u30e9\u30a4\u30a2\u30f3\u30c8 ID \u3092\u8ffd\u52a0] \u3092\u30af\u30ea\u30c3\u30af\u3057\u307e\u3059\u3002\n 1. \u65b0\u3057\u3044 OAuth \u30af\u30e9\u30a4\u30a2\u30f3\u30c8 ID \u3092\u5165\u529b\u3057\u3001[**\u8ffd\u52a0**] \u3092\u30af\u30ea\u30c3\u30af\u3057\u307e\u3059\u3002 \n\n\u3042\u306a\u305f\u306e OAuth \u30af\u30e9\u30a4\u30a2\u30f3\u30c8 ID \u306f\u6b21\u306e\u3068\u304a\u308a\u3067\u3059: ` {client_id} `", "title": "\u30cd\u30b9\u30c8: \u30c7\u30d0\u30a4\u30b9\u30a2\u30af\u30bb\u30b9\u30d7\u30ed\u30b8\u30a7\u30af\u30c8\u306e\u66f4\u65b0" }, "init": { @@ -96,6 +103,7 @@ "title": "Nest YAML\u306e\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" }, "removed_app_auth": { + "description": "\u30bb\u30ad\u30e5\u30ea\u30c6\u30a3\u3092\u5411\u4e0a\u3055\u305b\u3001\u30d5\u30a3\u30c3\u30b7\u30f3\u30b0\u306e\u30ea\u30b9\u30af\u3092\u8efd\u6e1b\u3059\u308b\u305f\u3081\u306b\u3001Google \u306f Home Assistant \u3067\u4f7f\u7528\u3055\u308c\u308b\u8a8d\u8a3c\u65b9\u6cd5\u3092\u5ec3\u6b62\u3057\u307e\u3057\u305f\u3002 \n\n **\u3053\u308c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001\u304a\u5ba2\u69d8\u306b\u3088\u308b\u30a2\u30af\u30b7\u30e7\u30f3\u304c\u5fc5\u8981\u3067\u3059** ([\u8a73\u7d30]( {more_info_url} )) \n\n 1.\u7d71\u5408\u30da\u30fc\u30b8\u306b\u30a2\u30af\u30bb\u30b9\u3057\u307e\u3059\n1. Nest \u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067 [\u518d\u69cb\u6210] \u3092\u30af\u30ea\u30c3\u30af\u3057\u307e\u3059\u3002\n 1. Home Assistant \u304c Web \u8a8d\u8a3c\u3078\u306e\u30a2\u30c3\u30d7\u30b0\u30ec\u30fc\u30c9\u624b\u9806\u3092\u6848\u5185\u3057\u307e\u3059\u3002 \n\n\u30c8\u30e9\u30d6\u30eb\u30b7\u30e5\u30fc\u30c6\u30a3\u30f3\u30b0\u60c5\u5831\u306b\u3064\u3044\u3066\u306f\u3001Nest \u306e [\u7d71\u5408\u624b\u9806]( {documentation_url} ) \u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "title": "Nest\u8a8d\u8a3c\u8cc7\u683c\u60c5\u5831\u3092\u66f4\u65b0\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059" } } diff --git a/homeassistant/components/nest/translations/nl.json b/homeassistant/components/nest/translations/nl.json index 8f5e0db9900..95ddcd4184d 100644 --- a/homeassistant/components/nest/translations/nl.json +++ b/homeassistant/components/nest/translations/nl.json @@ -67,5 +67,10 @@ "camera_sound": "Geluid gedetecteerd", "doorbell_chime": "Deurbel is ingedrukt" } + }, + "issues": { + "removed_app_auth": { + "description": "Om de beveiliging te verbeteren en het risico op phishing te verminderen, heeft Google de authenticatiemethode die door Home Assistant wordt gebruikt, be\u00ebindigd. \n\n **Dit vereist actie van jou om dit op te lossen** ([meer info]( {more_info_url} )) \n\n 1. Bezoek de integratiepagina.\n 2. Klik op Opnieuw configureren in de Nest-integratie.\n 3. Home Assistant leidt je door de stappen om te upgraden naar webauthenticatie. \n\n Zie de Nest [integratie-instructies]( {documentation_url} ) voor informatie over het oplossen van problemen." + } } } \ No newline at end of file diff --git a/homeassistant/components/nest/translations/pt.json b/homeassistant/components/nest/translations/pt.json index 40b70a2c67f..4e5c4c2c34a 100644 --- a/homeassistant/components/nest/translations/pt.json +++ b/homeassistant/components/nest/translations/pt.json @@ -50,5 +50,15 @@ "trigger_type": { "camera_motion": "Movimento detectado" } + }, + "issues": { + "deprecated_yaml": { + "description": "A configura\u00e7\u00e3o do Nest em configuration.yaml est\u00e1 sendo removida no Home Assistant 2022.10. \n\n Suas credenciais de aplicativo OAuth e configura\u00e7\u00f5es de acesso existentes foram importadas para a interface do usu\u00e1rio automaticamente. Remova a configura\u00e7\u00e3o YAML do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "A configura\u00e7\u00e3o Nest YAML est\u00e1 sendo removida" + }, + "removed_app_auth": { + "description": "Para melhorar a seguran\u00e7a e reduzir o risco de phishing, o Google desativou o m\u00e9todo de autentica\u00e7\u00e3o usado pelo Home Assistant. \n\n **Isso requer uma a\u00e7\u00e3o sua para resolver** ([mais informa\u00e7\u00f5es]( {more_info_url} )) \n\n 1. Visite a p\u00e1gina de integra\u00e7\u00f5es\n 1. Clique em Reconfigurar na integra\u00e7\u00e3o Nest.\n 1. O Home Assistant o guiar\u00e1 pelas etapas para atualizar para a autentica\u00e7\u00e3o da Web. \n\n Consulte as [instru\u00e7\u00f5es de integra\u00e7\u00e3o]( {documentation_url} ) do Nest para obter informa\u00e7\u00f5es sobre solu\u00e7\u00e3o de problemas.", + "title": "As credenciais de autentica\u00e7\u00e3o Nest precisam ser atualizadas" + } } } \ No newline at end of file diff --git a/homeassistant/components/netatmo/__init__.py b/homeassistant/components/netatmo/__init__.py index 36847c85515..eb0e93c4b38 100644 --- a/homeassistant/components/netatmo/__init__.py +++ b/homeassistant/components/netatmo/__init__.py @@ -8,6 +8,7 @@ import secrets import aiohttp import pyatmo +from pyatmo.const import ALL_SCOPES as NETATMO_SCOPES import voluptuous as vol from homeassistant.components import cloud @@ -51,7 +52,6 @@ from .const import ( DATA_PERSONS, DATA_SCHEDULES, DOMAIN, - NETATMO_SCOPES, PLATFORMS, WEBHOOK_DEACTIVATION, WEBHOOK_PUSH_TYPE, @@ -137,9 +137,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: raise ConfigEntryAuthFailed("Token not valid, trigger renewal") from ex raise ConfigEntryNotReady from ex - if sorted(session.token["scope"]) != sorted(NETATMO_SCOPES): + if entry.data["auth_implementation"] == cloud.DOMAIN: + required_scopes = { + scope + for scope in NETATMO_SCOPES + if scope not in ("access_doorbell", "read_doorbell") + } + else: + required_scopes = set(NETATMO_SCOPES) + + if not (set(session.token["scope"]) & required_scopes): _LOGGER.debug( - "Scope is invalid: %s != %s", session.token["scope"], NETATMO_SCOPES + "Session is missing scopes: %s", + required_scopes - set(session.token["scope"]), ) raise ConfigEntryAuthFailed("Token scope not valid, trigger renewal") @@ -150,10 +160,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: } data_handler = NetatmoDataHandler(hass, entry) - await data_handler.async_setup() hass.data[DOMAIN][entry.entry_id][DATA_HANDLER] = data_handler - - await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + await data_handler.async_setup() async def unregister_webhook( call_or_event_or_dt: ServiceCall | Event | datetime | None, @@ -208,10 +216,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: _LOGGER.info("Register Netatmo webhook: %s", webhook_url) except pyatmo.ApiError as err: _LOGGER.error("Error during webhook registration - %s", err) - - entry.async_on_unload( - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, unregister_webhook) - ) + else: + entry.async_on_unload( + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, unregister_webhook) + ) async def manage_cloudhook(state: cloud.CloudConnectionState) -> None: if state is cloud.CloudConnectionState.CLOUD_CONNECTED: diff --git a/homeassistant/components/netatmo/api.py b/homeassistant/components/netatmo/api.py index e13032dc399..0b36745338e 100644 --- a/homeassistant/components/netatmo/api.py +++ b/homeassistant/components/netatmo/api.py @@ -7,7 +7,7 @@ import pyatmo from homeassistant.helpers import config_entry_oauth2_flow -class AsyncConfigEntryNetatmoAuth(pyatmo.auth.AbstractAsyncAuth): +class AsyncConfigEntryNetatmoAuth(pyatmo.AbstractAsyncAuth): """Provide Netatmo authentication tied to an OAuth2 based config entry.""" def __init__( diff --git a/homeassistant/components/netatmo/camera.py b/homeassistant/components/netatmo/camera.py index 3235d16479c..9254ff6e284 100644 --- a/homeassistant/components/netatmo/camera.py +++ b/homeassistant/components/netatmo/camera.py @@ -5,13 +5,14 @@ import logging from typing import Any, cast import aiohttp -import pyatmo +from pyatmo import ApiError as NetatmoApiError, modules as NaModules +from pyatmo.event import Event as NaEvent import voluptuous as vol from homeassistant.components.camera import Camera, CameraEntityFeature from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import HomeAssistantError, PlatformNotReady +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -20,28 +21,24 @@ from .const import ( ATTR_CAMERA_LIGHT_MODE, ATTR_PERSON, ATTR_PERSONS, - ATTR_PSEUDO, CAMERA_LIGHT_MODES, + CONF_URL_SECURITY, DATA_CAMERAS, DATA_EVENTS, - DATA_HANDLER, - DATA_PERSONS, DOMAIN, EVENT_TYPE_LIGHT_MODE, EVENT_TYPE_OFF, EVENT_TYPE_ON, MANUFACTURER, - MODELS, + NETATMO_CREATE_CAMERA, SERVICE_SET_CAMERA_LIGHT, SERVICE_SET_PERSON_AWAY, SERVICE_SET_PERSONS_HOME, - SIGNAL_NAME, - TYPE_SECURITY, WEBHOOK_LIGHT_MODE, WEBHOOK_NACAMERA_CONNECTION, WEBHOOK_PUSH_TYPE, ) -from .data_handler import CAMERA_DATA_CLASS_NAME, NetatmoDataHandler +from .data_handler import EVENT, HOME, SIGNAL_NAME, NetatmoDevice from .netatmo_entity_base import NetatmoBase _LOGGER = logging.getLogger(__name__) @@ -53,42 +50,15 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the Netatmo camera platform.""" - data_handler = hass.data[DOMAIN][entry.entry_id][DATA_HANDLER] - data_class = data_handler.data.get(CAMERA_DATA_CLASS_NAME) + @callback + def _create_entity(netatmo_device: NetatmoDevice) -> None: + entity = NetatmoCamera(netatmo_device) + async_add_entities([entity]) - if not data_class or not data_class.raw_data: - raise PlatformNotReady - - all_cameras = [] - for home in data_class.cameras.values(): - for camera in home.values(): - all_cameras.append(camera) - - entities = [ - NetatmoCamera( - data_handler, - camera["id"], - camera["type"], - camera["home_id"], - DEFAULT_QUALITY, - ) - for camera in all_cameras - ] - - for home in data_class.homes.values(): - if home.get("id") is None: - continue - - hass.data[DOMAIN][DATA_PERSONS][home["id"]] = { - person_id: person_data.get(ATTR_PSEUDO) - for person_id, person_data in data_handler.data[CAMERA_DATA_CLASS_NAME] - .persons[home["id"]] - .items() - } - - _LOGGER.debug("Adding cameras %s", entities) - async_add_entities(entities, True) + entry.async_on_unload( + async_dispatcher_connect(hass, NETATMO_CREATE_CAMERA, _create_entity) + ) platform = entity_platform.async_get_current_platform() @@ -118,41 +88,44 @@ class NetatmoCamera(NetatmoBase, Camera): def __init__( self, - data_handler: NetatmoDataHandler, - camera_id: str, - camera_type: str, - home_id: str, - quality: str, + netatmo_device: NetatmoDevice, ) -> None: """Set up for access to the Netatmo camera images.""" Camera.__init__(self) - super().__init__(data_handler) + super().__init__(netatmo_device.data_handler) - self._publishers.append( - {"name": CAMERA_DATA_CLASS_NAME, SIGNAL_NAME: CAMERA_DATA_CLASS_NAME} - ) - - self._id = camera_id - self._home_id = home_id - self._device_name = self._data.get_camera(camera_id=camera_id)["name"] - self._model = camera_type - self._netatmo_type = TYPE_SECURITY + self._camera = cast(NaModules.Camera, netatmo_device.device) + self._id = self._camera.entity_id + self._home_id = self._camera.home.entity_id + self._device_name = self._camera.name + self._model = self._camera.device_type + self._config_url = CONF_URL_SECURITY self._attr_unique_id = f"{self._id}-{self._model}" - self._quality = quality - self._vpnurl: str | None = None - self._localurl: str | None = None - self._status: str | None = None - self._sd_status: str | None = None - self._alim_status: str | None = None - self._is_local: str | None = None + self._quality = DEFAULT_QUALITY + self._monitoring: bool | None = None self._light_state = None + self._publishers.extend( + [ + { + "name": HOME, + "home_id": self._home_id, + SIGNAL_NAME: f"{HOME}-{self._home_id}", + }, + { + "name": EVENT, + "home_id": self._home_id, + SIGNAL_NAME: f"{EVENT}-{self._home_id}", + }, + ] + ) + async def async_added_to_hass(self) -> None: """Entity created.""" await super().async_added_to_hass() for event_type in (EVENT_TYPE_LIGHT_MODE, EVENT_TYPE_OFF, EVENT_TYPE_ON): - self.data_handler.config_entry.async_on_unload( + self.async_on_remove( async_dispatcher_connect( self.hass, f"signal-{DOMAIN}-webhook-{event_type}", @@ -173,13 +146,13 @@ class NetatmoCamera(NetatmoBase, Camera): if data["home_id"] == self._home_id and data["camera_id"] == self._id: if data[WEBHOOK_PUSH_TYPE] in ("NACamera-off", "NACamera-disconnection"): self._attr_is_streaming = False - self._status = "off" + self._monitoring = False elif data[WEBHOOK_PUSH_TYPE] in ( "NACamera-on", WEBHOOK_NACAMERA_CONNECTION, ): self._attr_is_streaming = True - self._status = "on" + self._monitoring = True elif data[WEBHOOK_PUSH_TYPE] == WEBHOOK_LIGHT_MODE: self._light_state = data["sub_type"] self._attr_extra_state_attributes.update( @@ -189,128 +162,107 @@ class NetatmoCamera(NetatmoBase, Camera): self.async_write_ha_state() return - @property - def _data(self) -> pyatmo.AsyncCameraData: - """Return data for this entity.""" - return cast( - pyatmo.AsyncCameraData, - self.data_handler.data[self._publishers[0]["name"]], - ) - async def async_camera_image( self, width: int | None = None, height: int | None = None ) -> bytes | None: """Return a still image response from the camera.""" try: - return cast( - bytes, await self._data.async_get_live_snapshot(camera_id=self._id) - ) + return cast(bytes, await self._camera.async_get_live_snapshot()) except ( aiohttp.ClientPayloadError, aiohttp.ContentTypeError, aiohttp.ServerDisconnectedError, aiohttp.ClientConnectorError, - pyatmo.exceptions.ApiError, + NetatmoApiError, ) as err: _LOGGER.debug("Could not fetch live camera image (%s)", err) return None @property - def available(self) -> bool: - """Return True if entity is available.""" - return bool(self._alim_status == "on" or self._status == "disconnected") - - @property - def motion_detection_enabled(self) -> bool: - """Return the camera motion detection status.""" - return bool(self._status == "on") - - @property - def is_on(self) -> bool: - """Return true if on.""" - return self.is_streaming + def supported_features(self) -> int: + """Return supported features.""" + supported_features: int = CameraEntityFeature.ON_OFF + if self._model != "NDB": + supported_features |= CameraEntityFeature.STREAM + return supported_features async def async_turn_off(self) -> None: """Turn off camera.""" - await self._data.async_set_state( - home_id=self._home_id, camera_id=self._id, monitoring="off" - ) + await self._camera.async_monitoring_off() async def async_turn_on(self) -> None: """Turn on camera.""" - await self._data.async_set_state( - home_id=self._home_id, camera_id=self._id, monitoring="on" - ) + await self._camera.async_monitoring_on() async def stream_source(self) -> str: """Return the stream source.""" - url = "{0}/live/files/{1}/index.m3u8" - if self._localurl: - return url.format(self._localurl, self._quality) - return url.format(self._vpnurl, self._quality) + if self._camera.is_local: + await self._camera.async_update_camera_urls() - @property - def model(self) -> str: - """Return the camera model.""" - return MODELS[self._model] + if self._camera.local_url: + return "{}/live/files/{}/index.m3u8".format( + self._camera.local_url, self._quality + ) + return f"{self._camera.vpn_url}/live/files/{self._quality}/index.m3u8" @callback def async_update_callback(self) -> None: """Update the entity's state.""" - camera = self._data.get_camera(self._id) - self._vpnurl, self._localurl = self._data.camera_urls(self._id) - self._status = camera.get("status") - self._sd_status = camera.get("sd_status") - self._alim_status = camera.get("alim_status") - self._is_local = camera.get("is_local") - self._attr_is_streaming = bool(self._status == "on") + self._attr_is_on = self._camera.alim_status is not None + self._attr_available = self._camera.alim_status is not None - if self._model == "NACamera": # Smart Indoor Camera - self.hass.data[DOMAIN][DATA_EVENTS][self._id] = self.process_events( - self._data.events.get(self._id, {}) - ) - elif self._model == "NOC": # Smart Outdoor Camera - self.hass.data[DOMAIN][DATA_EVENTS][self._id] = self.process_events( - self._data.outdoor_events.get(self._id, {}) - ) + if self._camera.monitoring is not None: + self._attr_is_streaming = self._camera.monitoring + self._attr_motion_detection_enabled = self._camera.monitoring + + self.hass.data[DOMAIN][DATA_EVENTS][self._id] = self.process_events( + self._camera.events + ) self._attr_extra_state_attributes.update( { "id": self._id, - "status": self._status, - "sd_status": self._sd_status, - "alim_status": self._alim_status, - "is_local": self._is_local, - "vpn_url": self._vpnurl, - "local_url": self._localurl, + "monitoring": self._monitoring, + "sd_status": self._camera.sd_status, + "alim_status": self._camera.alim_status, + "is_local": self._camera.is_local, + "vpn_url": self._camera.vpn_url, + "local_url": self._camera.local_url, "light_state": self._light_state, } ) - def process_events(self, events: dict) -> dict: + def process_events(self, event_list: list[NaEvent]) -> dict: """Add meta data to events.""" - for event in events.values(): - if "video_id" not in event: + events = {} + for event in event_list: + if not (video_id := event.video_id): continue - if self._is_local: - event[ - "media_url" - ] = f"{self._localurl}/vod/{event['video_id']}/files/{self._quality}/index.m3u8" - else: - event[ - "media_url" - ] = f"{self._vpnurl}/vod/{event['video_id']}/files/{self._quality}/index.m3u8" + event_data = event.__dict__ + event_data["subevents"] = [ + event.__dict__ + for event in event_data.get("subevents", []) + if not isinstance(event, dict) + ] + event_data["media_url"] = self.get_video_url(video_id) + events[event.event_time] = event_data return events + def get_video_url(self, video_id: str) -> str: + """Get video url.""" + if self._camera.is_local: + return f"{self._camera.local_url}/vod/{video_id}/files/{self._quality}/index.m3u8" + return f"{self._camera.vpn_url}/vod/{video_id}/files/{self._quality}/index.m3u8" + def fetch_person_ids(self, persons: list[str | None]) -> list[str]: - """Fetch matching person ids for give list of persons.""" + """Fetch matching person ids for given list of persons.""" person_ids = [] person_id_errors = [] for person in persons: person_id = None - for pid, data in self._data.persons[self._home_id].items(): - if data.get("pseudo") == person: + for pid, data in self._camera.home.persons.items(): + if data.pseudo == person: person_ids.append(pid) person_id = pid break @@ -328,9 +280,7 @@ class NetatmoCamera(NetatmoBase, Camera): persons = kwargs.get(ATTR_PERSONS, []) person_ids = self.fetch_person_ids(persons) - await self._data.async_set_persons_home( - person_ids=person_ids, home_id=self._home_id - ) + await self._camera.home.async_set_persons_home(person_ids=person_ids) _LOGGER.debug("Set %s as at home", persons) async def _service_set_person_away(self, **kwargs: Any) -> None: @@ -339,9 +289,8 @@ class NetatmoCamera(NetatmoBase, Camera): person_ids = self.fetch_person_ids([person] if person else []) person_id = next(iter(person_ids), None) - await self._data.async_set_persons_away( + await self._camera.home.async_set_persons_away( person_id=person_id, - home_id=self._home_id, ) if person_id: @@ -351,10 +300,11 @@ class NetatmoCamera(NetatmoBase, Camera): async def _service_set_camera_light(self, **kwargs: Any) -> None: """Service to set light mode.""" + if not isinstance(self._camera, NaModules.netatmo.NOC): + raise HomeAssistantError( + f"{self._model} <{self._device_name}> does not have a floodlight" + ) + mode = str(kwargs.get(ATTR_CAMERA_LIGHT_MODE)) _LOGGER.debug("Turn %s camera light for '%s'", mode, self._attr_name) - await self._data.async_set_state( - home_id=self._home_id, - camera_id=self._id, - floodlight=mode, - ) + await self._camera.async_set_floodlight_state(mode) diff --git a/homeassistant/components/netatmo/climate.py b/homeassistant/components/netatmo/climate.py index eb7d996eefb..400004ee4d1 100644 --- a/homeassistant/components/netatmo/climate.py +++ b/homeassistant/components/netatmo/climate.py @@ -2,16 +2,17 @@ from __future__ import annotations import logging -from typing import Any +from typing import Any, cast -import pyatmo +from pyatmo.modules import NATherm1 import voluptuous as vol -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( DEFAULT_MIN_TEMP, PRESET_AWAY, PRESET_BOOST, + PRESET_HOME, + ClimateEntity, ClimateEntityFeature, HVACAction, HVACMode, @@ -25,12 +26,8 @@ from homeassistant.const import ( TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers import config_validation as cv, entity_platform -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, - async_dispatcher_send, -) +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -38,25 +35,17 @@ from .const import ( ATTR_HEATING_POWER_REQUEST, ATTR_SCHEDULE_NAME, ATTR_SELECTED_SCHEDULE, - DATA_HANDLER, - DATA_HOMES, + CONF_URL_ENERGY, DATA_SCHEDULES, DOMAIN, EVENT_TYPE_CANCEL_SET_POINT, EVENT_TYPE_SCHEDULE, EVENT_TYPE_SET_POINT, EVENT_TYPE_THERM_MODE, - NETATMO_CREATE_BATTERY, + NETATMO_CREATE_CLIMATE, SERVICE_SET_SCHEDULE, - SIGNAL_NAME, - TYPE_ENERGY, -) -from .data_handler import ( - CLIMATE_STATE_CLASS_NAME, - CLIMATE_TOPOLOGY_CLASS_NAME, - NetatmoDataHandler, - NetatmoDevice, ) +from .data_handler import HOME, SIGNAL_NAME, NetatmoRoom from .netatmo_entity_base import NetatmoBase _LOGGER = logging.getLogger(__name__) @@ -65,6 +54,9 @@ PRESET_FROST_GUARD = "Frost Guard" PRESET_SCHEDULE = "Schedule" PRESET_MANUAL = "Manual" +SUPPORT_FLAGS = ( + ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE +) SUPPORT_PRESET = [PRESET_AWAY, PRESET_BOOST, PRESET_FROST_GUARD, PRESET_SCHEDULE] STATE_NETATMO_SCHEDULE = "schedule" @@ -116,51 +108,22 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the Netatmo energy platform.""" - data_handler = hass.data[DOMAIN][entry.entry_id][DATA_HANDLER] - climate_topology = data_handler.data.get(CLIMATE_TOPOLOGY_CLASS_NAME) + @callback + def _create_entity(netatmo_device: NetatmoRoom) -> None: + entity = NetatmoThermostat(netatmo_device) + async_add_entities([entity]) - if not climate_topology or climate_topology.raw_data == {}: - raise PlatformNotReady - - entities = [] - for home_id in climate_topology.home_ids: - signal_name = f"{CLIMATE_STATE_CLASS_NAME}-{home_id}" - - await data_handler.subscribe( - CLIMATE_STATE_CLASS_NAME, signal_name, None, home_id=home_id - ) - - if (climate_state := data_handler.data[signal_name]) is None: - continue - - climate_topology.register_handler(home_id, climate_state.process_topology) - - for room in climate_state.homes[home_id].rooms.values(): - if room.device_type is None or room.device_type.value not in [ - NA_THERM, - NA_VALVE, - ]: - continue - entities.append(NetatmoThermostat(data_handler, room)) - - hass.data[DOMAIN][DATA_SCHEDULES][home_id] = climate_state.homes[ - home_id - ].schedules - - hass.data[DOMAIN][DATA_HOMES][home_id] = climate_state.homes[home_id].name - - _LOGGER.debug("Adding climate devices %s", entities) - async_add_entities(entities, True) + entry.async_on_unload( + async_dispatcher_connect(hass, NETATMO_CREATE_CLIMATE, _create_entity) + ) platform = entity_platform.async_get_current_platform() - - if climate_topology is not None: - platform.async_register_entity_service( - SERVICE_SET_SCHEDULE, - {vol.Required(ATTR_SCHEDULE_NAME): cv.string}, - "_async_service_set_schedule", - ) + platform.async_register_entity_service( + SERVICE_SET_SCHEDULE, + {vol.Required(ATTR_SCHEDULE_NAME): cv.string}, + "_async_service_set_schedule", + ) class NetatmoThermostat(NetatmoBase, ClimateEntity): @@ -169,42 +132,33 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): _attr_hvac_mode = HVACMode.AUTO _attr_max_temp = DEFAULT_MAX_TEMP _attr_preset_modes = SUPPORT_PRESET - _attr_supported_features = ( - ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE - ) + _attr_supported_features = SUPPORT_FLAGS _attr_target_temperature_step = PRECISION_HALVES _attr_temperature_unit = TEMP_CELSIUS - def __init__( - self, data_handler: NetatmoDataHandler, room: pyatmo.climate.NetatmoRoom - ) -> None: + def __init__(self, netatmo_device: NetatmoRoom) -> None: """Initialize the sensor.""" ClimateEntity.__init__(self) - super().__init__(data_handler) + super().__init__(netatmo_device.data_handler) - self._room = room + self._room = netatmo_device.room self._id = self._room.entity_id + self._home_id = self._room.home.entity_id - self._signal_name = f"{CLIMATE_STATE_CLASS_NAME}-{self._room.home.entity_id}" - self._climate_state: pyatmo.AsyncClimate = data_handler.data[self._signal_name] - + self._signal_name = f"{HOME}-{self._home_id}" self._publishers.extend( [ { - "name": CLIMATE_TOPOLOGY_CLASS_NAME, - SIGNAL_NAME: CLIMATE_TOPOLOGY_CLASS_NAME, - }, - { - "name": CLIMATE_STATE_CLASS_NAME, + "name": HOME, "home_id": self._room.home.entity_id, SIGNAL_NAME: self._signal_name, }, ] ) - self._model: str = getattr(room.device_type, "value") + self._model: str = f"{self._room.climate_type}" - self._netatmo_type = TYPE_ENERGY + self._config_url = CONF_URL_ENERGY self._attr_name = self._room.name self._away: bool | None = None @@ -231,7 +185,7 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): EVENT_TYPE_CANCEL_SET_POINT, EVENT_TYPE_SCHEDULE, ): - self.data_handler.config_entry.async_on_unload( + self.async_on_remove( async_dispatcher_connect( self.hass, f"signal-{DOMAIN}-webhook-{event_type}", @@ -239,21 +193,6 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): ) ) - for module in self._room.modules.values(): - if getattr(module.device_type, "value") not in [NA_THERM, NA_VALVE]: - continue - - async_dispatcher_send( - self.hass, - NETATMO_CREATE_BATTERY, - NetatmoDevice( - self.data_handler, - module, - self._id, - self._signal_name, - ), - ) - @callback def handle_event(self, event: dict) -> None: """Handle webhook events.""" @@ -289,7 +228,7 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): self._attr_target_temperature = self._hg_temperature elif self._attr_preset_mode == PRESET_AWAY: self._attr_target_temperature = self._away_temperature - elif self._attr_preset_mode == PRESET_SCHEDULE: + elif self._attr_preset_mode in [PRESET_SCHEDULE, PRESET_HOME]: self.async_update_callback() self.data_handler.async_force_update(self._signal_name) self.async_write_ha_state() @@ -322,6 +261,10 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): data["event_type"] == EVENT_TYPE_CANCEL_SET_POINT and self._room.entity_id == room["id"] ): + if self._attr_hvac_mode == HVACMode.OFF: + self._attr_hvac_mode = HVACMode.AUTO + self._attr_preset_mode = PRESET_MAP_NETATMO[PRESET_SCHEDULE] + self.async_update_callback() self.async_write_ha_state() return @@ -329,7 +272,7 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): @property def hvac_action(self) -> HVACAction: """Return the current running hvac operation if supported.""" - if self._model == NA_THERM and self._boilerstatus is not None: + if self._model != NA_VALVE and self._boilerstatus is not None: return CURRENT_HVAC_MAP_NETATMO[self._boilerstatus] # Maybe it is a valve if ( @@ -343,55 +286,36 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): if hvac_mode == HVACMode.OFF: await self.async_turn_off() elif hvac_mode == HVACMode.AUTO: - if self.hvac_mode == HVACMode.OFF: - await self.async_turn_on() await self.async_set_preset_mode(PRESET_SCHEDULE) elif hvac_mode == HVACMode.HEAT: await self.async_set_preset_mode(PRESET_BOOST) async def async_set_preset_mode(self, preset_mode: str) -> None: """Set new preset mode.""" - if self.hvac_mode == HVACMode.OFF: - await self.async_turn_on() - - if self.target_temperature == 0: - await self._climate_state.async_set_room_thermpoint( - self._room.entity_id, - STATE_NETATMO_HOME, - ) - if ( preset_mode in (PRESET_BOOST, STATE_NETATMO_MAX) and self._model == NA_VALVE - and self.hvac_mode == HVACMode.HEAT + and self._attr_hvac_mode == HVACMode.HEAT ): - await self._climate_state.async_set_room_thermpoint( - self._room.entity_id, + await self._room.async_therm_set( STATE_NETATMO_HOME, ) elif ( preset_mode in (PRESET_BOOST, STATE_NETATMO_MAX) and self._model == NA_VALVE ): - await self._climate_state.async_set_room_thermpoint( - self._room.entity_id, + await self._room.async_therm_set( STATE_NETATMO_MANUAL, DEFAULT_MAX_TEMP, ) elif ( preset_mode in (PRESET_BOOST, STATE_NETATMO_MAX) - and self.hvac_mode == HVACMode.HEAT + and self._attr_hvac_mode == HVACMode.HEAT ): - await self._climate_state.async_set_room_thermpoint( - self._room.entity_id, STATE_NETATMO_HOME - ) + await self._room.async_therm_set(STATE_NETATMO_HOME) elif preset_mode in (PRESET_BOOST, STATE_NETATMO_MAX): - await self._climate_state.async_set_room_thermpoint( - self._room.entity_id, PRESET_MAP_NETATMO[preset_mode] - ) + await self._room.async_therm_set(PRESET_MAP_NETATMO[preset_mode]) elif preset_mode in (PRESET_SCHEDULE, PRESET_FROST_GUARD, PRESET_AWAY): - await self._climate_state.async_set_thermmode( - PRESET_MAP_NETATMO[preset_mode] - ) + await self._room.async_therm_set(PRESET_MAP_NETATMO[preset_mode]) else: _LOGGER.error("Preset mode '%s' not available", preset_mode) @@ -399,33 +323,25 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature for 2 hours.""" - if (temp := kwargs.get(ATTR_TEMPERATURE)) is None: - return - await self._climate_state.async_set_room_thermpoint( - self._room.entity_id, STATE_NETATMO_MANUAL, min(temp, DEFAULT_MAX_TEMP) + await self._room.async_therm_set( + STATE_NETATMO_MANUAL, min(kwargs[ATTR_TEMPERATURE], DEFAULT_MAX_TEMP) ) - self.async_write_ha_state() async def async_turn_off(self) -> None: """Turn the entity off.""" if self._model == NA_VALVE: - await self._climate_state.async_set_room_thermpoint( - self._room.entity_id, + await self._room.async_therm_set( STATE_NETATMO_MANUAL, DEFAULT_MIN_TEMP, ) - elif self.hvac_mode != HVACMode.OFF: - await self._climate_state.async_set_room_thermpoint( - self._room.entity_id, STATE_NETATMO_OFF - ) + elif self._attr_hvac_mode != HVACMode.OFF: + await self._room.async_therm_set(STATE_NETATMO_OFF) self.async_write_ha_state() async def async_turn_on(self) -> None: """Turn the entity on.""" - await self._climate_state.async_set_room_thermpoint( - self._room.entity_id, STATE_NETATMO_HOME - ) + await self._room.async_therm_set(STATE_NETATMO_HOME) self.async_write_ha_state() @property @@ -466,8 +382,11 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): ] = self._room.heating_power_request else: for module in self._room.modules.values(): - self._boilerstatus = module.boiler_status - break + if hasattr(module, "boiler_status"): + module = cast(NATherm1, module) + if module.boiler_status is not None: + self._boilerstatus = module.boiler_status + break async def _async_service_set_schedule(self, **kwargs: Any) -> None: schedule_name = kwargs.get(ATTR_SCHEDULE_NAME) @@ -483,7 +402,7 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): _LOGGER.error("%s is not a valid schedule", kwargs.get(ATTR_SCHEDULE_NAME)) return - await self._climate_state.async_switch_home_schedule(schedule_id=schedule_id) + await self._room.home.async_switch_schedule(schedule_id=schedule_id) _LOGGER.debug( "Setting %s schedule to %s (%s)", self._room.home.entity_id, diff --git a/homeassistant/components/netatmo/config_flow.py b/homeassistant/components/netatmo/config_flow.py index ba63c76ad66..acd8965d013 100644 --- a/homeassistant/components/netatmo/config_flow.py +++ b/homeassistant/components/netatmo/config_flow.py @@ -6,6 +6,7 @@ import logging from typing import Any import uuid +from pyatmo.const import ALL_SCOPES import voluptuous as vol from homeassistant import config_entries @@ -25,7 +26,6 @@ from .const import ( CONF_UUID, CONF_WEATHER_AREAS, DOMAIN, - NETATMO_SCOPES, ) _LOGGER = logging.getLogger(__name__) @@ -54,7 +54,14 @@ class NetatmoFlowHandler( @property def extra_authorize_data(self) -> dict: """Extra data that needs to be appended to the authorize url.""" - return {"scope": " ".join(NETATMO_SCOPES)} + exclude = [] + if self.flow_impl.name == "Home Assistant Cloud": + exclude = ["access_doorbell", "read_doorbell"] + + scopes = [scope for scope in ALL_SCOPES if scope not in exclude] + scopes.sort() + + return {"scope": " ".join(scopes)} async def async_step_user(self, user_input: dict | None = None) -> FlowResult: """Handle a flow start.""" diff --git a/homeassistant/components/netatmo/const.py b/homeassistant/components/netatmo/const.py index 6bd66fa9644..e93d0c91a07 100644 --- a/homeassistant/components/netatmo/const.py +++ b/homeassistant/components/netatmo/const.py @@ -10,60 +10,17 @@ DEFAULT_ATTRIBUTION = f"Data provided by {MANUFACTURER}" PLATFORMS = [ Platform.CAMERA, Platform.CLIMATE, + Platform.COVER, Platform.LIGHT, Platform.SELECT, Platform.SENSOR, + Platform.SWITCH, ] -NETATMO_SCOPES = [ - "access_camera", - "access_presence", - "read_camera", - "read_homecoach", - "read_presence", - "read_smokedetector", - "read_station", - "read_thermostat", - "write_camera", - "write_presence", - "write_thermostat", -] - -MODEL_NAPLUG = "Relay" -MODEL_NATHERM1 = "Smart Thermostat" -MODEL_NRV = "Smart Radiator Valves" -MODEL_NOC = "Smart Outdoor Camera" -MODEL_NACAMERA = "Smart Indoor Camera" -MODEL_NSD = "Smart Smoke Alarm" -MODEL_NACAMDOORTAG = "Smart Door and Window Sensors" -MODEL_NHC = "Smart Indoor Air Quality Monitor" -MODEL_NAMAIN = "Smart Home Weather station – indoor module" -MODEL_NAMODULE1 = "Smart Home Weather station – outdoor module" -MODEL_NAMODULE4 = "Smart Additional Indoor module" -MODEL_NAMODULE3 = "Smart Rain Gauge" -MODEL_NAMODULE2 = "Smart Anemometer" -MODEL_PUBLIC = "Public Weather stations" - -MODELS = { - "NAPlug": MODEL_NAPLUG, - "NATherm1": MODEL_NATHERM1, - "NRV": MODEL_NRV, - "NACamera": MODEL_NACAMERA, - "NOC": MODEL_NOC, - "NSD": MODEL_NSD, - "NACamDoorTag": MODEL_NACAMDOORTAG, - "NHC": MODEL_NHC, - "NAMain": MODEL_NAMAIN, - "NAModule1": MODEL_NAMODULE1, - "NAModule4": MODEL_NAMODULE4, - "NAModule3": MODEL_NAMODULE3, - "NAModule2": MODEL_NAMODULE2, - "public": MODEL_PUBLIC, -} - -TYPE_SECURITY = "security" -TYPE_ENERGY = "energy" -TYPE_WEATHER = "weather" +CONF_URL_SECURITY = "https://home.netatmo.com/security" +CONF_URL_ENERGY = "https://my.netatmo.com/app/energy" +CONF_URL_WEATHER = "https://my.netatmo.com/app/weather" +CONF_URL_CONTROL = "https://home.netatmo.com/control" AUTH = "netatmo_auth" CONF_PUBLIC = "public_sensor_config" @@ -71,7 +28,18 @@ CAMERA_DATA = "netatmo_camera" HOME_DATA = "netatmo_home_data" DATA_HANDLER = "netatmo_data_handler" SIGNAL_NAME = "signal_name" + NETATMO_CREATE_BATTERY = "netatmo_create_battery" +NETATMO_CREATE_CAMERA = "netatmo_create_camera" +NETATMO_CREATE_CAMERA_LIGHT = "netatmo_create_camera_light" +NETATMO_CREATE_CLIMATE = "netatmo_create_climate" +NETATMO_CREATE_COVER = "netatmo_create_cover" +NETATMO_CREATE_LIGHT = "netatmo_create_light" +NETATMO_CREATE_ROOM_SENSOR = "netatmo_create_room_sensor" +NETATMO_CREATE_SELECT = "netatmo_create_select" +NETATMO_CREATE_SENSOR = "netatmo_create_sensor" +NETATMO_CREATE_SWITCH = "netatmo_create_switch" +NETATMO_CREATE_WEATHER_SENSOR = "netatmo_create_weather_sensor" CONF_AREA_NAME = "area_name" CONF_CLOUDHOOK_URL = "cloudhook_url" diff --git a/homeassistant/components/netatmo/cover.py b/homeassistant/components/netatmo/cover.py new file mode 100644 index 00000000000..6d755d828d3 --- /dev/null +++ b/homeassistant/components/netatmo/cover.py @@ -0,0 +1,110 @@ +"""Support for Netatmo/Bubendorff covers.""" +from __future__ import annotations + +import logging +from typing import Any, cast + +from pyatmo import modules as NaModules + +from homeassistant.components.cover import ( + ATTR_POSITION, + CoverDeviceClass, + CoverEntity, + CoverEntityFeature, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import CONF_URL_CONTROL, NETATMO_CREATE_COVER +from .data_handler import HOME, SIGNAL_NAME, NetatmoDevice +from .netatmo_entity_base import NetatmoBase + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Netatmo cover platform.""" + + @callback + def _create_entity(netatmo_device: NetatmoDevice) -> None: + entity = NetatmoCover(netatmo_device) + _LOGGER.debug("Adding cover %s", entity) + async_add_entities([entity]) + + entry.async_on_unload( + async_dispatcher_connect(hass, NETATMO_CREATE_COVER, _create_entity) + ) + + +class NetatmoCover(NetatmoBase, CoverEntity): + """Representation of a Netatmo cover device.""" + + _attr_supported_features = ( + CoverEntityFeature.OPEN + | CoverEntityFeature.CLOSE + | CoverEntityFeature.STOP + | CoverEntityFeature.SET_POSITION + ) + + def __init__(self, netatmo_device: NetatmoDevice) -> None: + """Initialize the Netatmo device.""" + super().__init__(netatmo_device.data_handler) + + self._cover = cast(NaModules.Shutter, netatmo_device.device) + + self._id = self._cover.entity_id + self._attr_name = self._device_name = self._cover.name + self._model = self._cover.device_type + self._config_url = CONF_URL_CONTROL + + self._home_id = self._cover.home.entity_id + self._attr_is_closed = self._cover.current_position == 0 + + self._signal_name = f"{HOME}-{self._home_id}" + self._publishers.extend( + [ + { + "name": HOME, + "home_id": self._home_id, + SIGNAL_NAME: self._signal_name, + }, + ] + ) + self._attr_unique_id = f"{self._id}-{self._model}" + + async def async_close_cover(self, **kwargs: Any) -> None: + """Close the cover.""" + await self._cover.async_close() + self._attr_is_closed = True + self.async_write_ha_state() + + async def async_open_cover(self, **kwargs: Any) -> None: + """Open the cover.""" + await self._cover.async_open() + self._attr_is_closed = False + self.async_write_ha_state() + + async def async_stop_cover(self, **kwargs: Any) -> None: + """Stop the cover.""" + await self._cover.async_stop() + + async def async_set_cover_position(self, **kwargs: Any) -> None: + """Move the cover shutter to a specific position.""" + await self._cover.async_set_target_position(kwargs[ATTR_POSITION]) + + @property + def device_class(self) -> str: + """Return the device class.""" + return CoverDeviceClass.SHUTTER + + @callback + def async_update_callback(self) -> None: + """Update the entity's state.""" + self._attr_is_closed = self._cover.current_position == 0 + self._attr_current_cover_position = self._cover.current_position diff --git a/homeassistant/components/netatmo/data_handler.py b/homeassistant/components/netatmo/data_handler.py index 50a3bed17ff..a376e6ee187 100644 --- a/homeassistant/components/netatmo/data_handler.py +++ b/homeassistant/components/netatmo/data_handler.py @@ -11,16 +11,34 @@ from time import time from typing import Any import pyatmo +from pyatmo.modules.device_types import DeviceCategory as NetatmoDeviceCategory from homeassistant.config_entries import ConfigEntry from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) from homeassistant.helpers.event import async_track_time_interval from .const import ( AUTH, + DATA_PERSONS, + DATA_SCHEDULES, DOMAIN, MANUFACTURER, + NETATMO_CREATE_BATTERY, + NETATMO_CREATE_CAMERA, + NETATMO_CREATE_CAMERA_LIGHT, + NETATMO_CREATE_CLIMATE, + NETATMO_CREATE_COVER, + NETATMO_CREATE_LIGHT, + NETATMO_CREATE_ROOM_SENSOR, + NETATMO_CREATE_SELECT, + NETATMO_CREATE_SENSOR, + NETATMO_CREATE_SWITCH, + NETATMO_CREATE_WEATHER_SENSOR, + PLATFORMS, WEBHOOK_ACTIVATION, WEBHOOK_DEACTIVATION, WEBHOOK_NACAMERA_CONNECTION, @@ -29,30 +47,31 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -CAMERA_DATA_CLASS_NAME = "AsyncCameraData" -WEATHERSTATION_DATA_CLASS_NAME = "AsyncWeatherStationData" -HOMECOACH_DATA_CLASS_NAME = "AsyncHomeCoachData" -CLIMATE_TOPOLOGY_CLASS_NAME = "AsyncClimateTopology" -CLIMATE_STATE_CLASS_NAME = "AsyncClimate" -PUBLICDATA_DATA_CLASS_NAME = "AsyncPublicData" +SIGNAL_NAME = "signal_name" +ACCOUNT = "account" +HOME = "home" +WEATHER = "weather" +AIR_CARE = "air_care" +PUBLIC = "public" +EVENT = "event" -DATA_CLASSES = { - WEATHERSTATION_DATA_CLASS_NAME: pyatmo.AsyncWeatherStationData, - HOMECOACH_DATA_CLASS_NAME: pyatmo.AsyncHomeCoachData, - CAMERA_DATA_CLASS_NAME: pyatmo.AsyncCameraData, - CLIMATE_TOPOLOGY_CLASS_NAME: pyatmo.AsyncClimateTopology, - CLIMATE_STATE_CLASS_NAME: pyatmo.AsyncClimate, - PUBLICDATA_DATA_CLASS_NAME: pyatmo.AsyncPublicData, +PUBLISHERS = { + ACCOUNT: "async_update_topology", + HOME: "async_update_status", + WEATHER: "async_update_weather_stations", + AIR_CARE: "async_update_air_care", + PUBLIC: "async_update_public_weather", + EVENT: "async_update_events", } BATCH_SIZE = 3 DEFAULT_INTERVALS = { - CLIMATE_TOPOLOGY_CLASS_NAME: 3600, - CLIMATE_STATE_CLASS_NAME: 300, - CAMERA_DATA_CLASS_NAME: 900, - WEATHERSTATION_DATA_CLASS_NAME: 600, - HOMECOACH_DATA_CLASS_NAME: 300, - PUBLICDATA_DATA_CLASS_NAME: 600, + ACCOUNT: 10800, + HOME: 300, + WEATHER: 600, + AIR_CARE: 300, + PUBLIC: 600, + EVENT: 600, } SCAN_INTERVAL = 60 @@ -62,7 +81,27 @@ class NetatmoDevice: """Netatmo device class.""" data_handler: NetatmoDataHandler - device: pyatmo.climate.NetatmoModule + device: pyatmo.modules.Module + parent_id: str + signal_name: str + + +@dataclass +class NetatmoHome: + """Netatmo home class.""" + + data_handler: NetatmoDataHandler + home: pyatmo.Home + parent_id: str + signal_name: str + + +@dataclass +class NetatmoRoom: + """Netatmo room class.""" + + data_handler: NetatmoDataHandler + room: pyatmo.Room parent_id: str signal_name: str @@ -74,25 +113,27 @@ class NetatmoPublisher: name: str interval: int next_scan: float - subscriptions: list[CALLBACK_TYPE | None] + subscriptions: set[CALLBACK_TYPE | None] + method: str + kwargs: dict class NetatmoDataHandler: """Manages the Netatmo data handling.""" + account: pyatmo.AsyncAccount + def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None: """Initialize self.""" self.hass = hass self.config_entry = config_entry self._auth = hass.data[DOMAIN][config_entry.entry_id][AUTH] self.publisher: dict[str, NetatmoPublisher] = {} - self.data: dict = {} self._queue: deque = deque() self._webhook: bool = False async def async_setup(self) -> None: """Set up the Netatmo data handler.""" - async_track_time_interval( self.hass, self.async_update, timedelta(seconds=SCAN_INTERVAL) ) @@ -105,17 +146,14 @@ class NetatmoDataHandler: ) ) - await asyncio.gather( - *[ - self.subscribe(data_class, data_class, None) - for data_class in ( - CLIMATE_TOPOLOGY_CLASS_NAME, - CAMERA_DATA_CLASS_NAME, - WEATHERSTATION_DATA_CLASS_NAME, - HOMECOACH_DATA_CLASS_NAME, - ) - ] + self.account = pyatmo.AsyncAccount(self._auth) + + await self.subscribe(ACCOUNT, ACCOUNT, None) + + await self.hass.config_entries.async_forward_entry_setups( + self.config_entry, PLATFORMS ) + await self.async_dispatch() async def async_update(self, event_time: datetime) -> None: """ @@ -153,19 +191,17 @@ class NetatmoDataHandler: elif event["data"][WEBHOOK_PUSH_TYPE] == WEBHOOK_NACAMERA_CONNECTION: _LOGGER.debug("%s camera reconnected", MANUFACTURER) - self.async_force_update(CAMERA_DATA_CLASS_NAME) + self.async_force_update(ACCOUNT) async def async_fetch_data(self, signal_name: str) -> None: """Fetch data and notify.""" - if self.data[signal_name] is None: - return - try: - await self.data[signal_name].async_update() + await getattr(self.account, self.publisher[signal_name].method)( + **self.publisher[signal_name].kwargs + ) except pyatmo.NoDevice as err: _LOGGER.debug(err) - self.data[signal_name] = None except pyatmo.ApiError as err: _LOGGER.debug(err) @@ -188,18 +224,21 @@ class NetatmoDataHandler: """Subscribe to publisher.""" if signal_name in self.publisher: if update_callback not in self.publisher[signal_name].subscriptions: - self.publisher[signal_name].subscriptions.append(update_callback) + self.publisher[signal_name].subscriptions.add(update_callback) return + if publisher == "public": + kwargs = {"area_id": self.account.register_public_weather_area(**kwargs)} + self.publisher[signal_name] = NetatmoPublisher( name=signal_name, interval=DEFAULT_INTERVALS[publisher], next_scan=time() + DEFAULT_INTERVALS[publisher], - subscriptions=[update_callback], + subscriptions={update_callback}, + method=PUBLISHERS[publisher], + kwargs=kwargs, ) - self.data[signal_name] = DATA_CLASSES[publisher](self._auth, **kwargs) - try: await self.async_fetch_data(signal_name) except KeyError: @@ -213,15 +252,158 @@ class NetatmoDataHandler: self, signal_name: str, update_callback: CALLBACK_TYPE | None ) -> None: """Unsubscribe from publisher.""" + if update_callback in self.publisher[signal_name].subscriptions: + return + self.publisher[signal_name].subscriptions.remove(update_callback) if not self.publisher[signal_name].subscriptions: self._queue.remove(self.publisher[signal_name]) self.publisher.pop(signal_name) - self.data.pop(signal_name) _LOGGER.debug("Publisher %s removed", signal_name) @property def webhook(self) -> bool: """Return the webhook state.""" return self._webhook + + async def async_dispatch(self) -> None: + """Dispatch the creation of entities.""" + await self.subscribe(WEATHER, WEATHER, None) + await self.subscribe(AIR_CARE, AIR_CARE, None) + + self.setup_air_care() + + for home in self.account.homes.values(): + signal_home = f"{HOME}-{home.entity_id}" + + await self.subscribe(HOME, signal_home, None, home_id=home.entity_id) + await self.subscribe(EVENT, signal_home, None, home_id=home.entity_id) + + self.setup_climate_schedule_select(home, signal_home) + self.setup_rooms(home, signal_home) + self.setup_modules(home, signal_home) + + self.hass.data[DOMAIN][DATA_PERSONS][home.entity_id] = { + person.entity_id: person.pseudo for person in home.persons.values() + } + + def setup_air_care(self) -> None: + """Set up home coach/air care modules.""" + for module in self.account.modules.values(): + if module.device_category is NetatmoDeviceCategory.air_care: + async_dispatcher_send( + self.hass, + NETATMO_CREATE_WEATHER_SENSOR, + NetatmoDevice( + self, + module, + AIR_CARE, + AIR_CARE, + ), + ) + + def setup_modules(self, home: pyatmo.Home, signal_home: str) -> None: + """Set up modules.""" + netatmo_type_signal_map = { + NetatmoDeviceCategory.camera: [ + NETATMO_CREATE_CAMERA, + NETATMO_CREATE_CAMERA_LIGHT, + ], + NetatmoDeviceCategory.dimmer: [NETATMO_CREATE_LIGHT], + NetatmoDeviceCategory.shutter: [NETATMO_CREATE_COVER], + NetatmoDeviceCategory.switch: [ + NETATMO_CREATE_LIGHT, + NETATMO_CREATE_SWITCH, + NETATMO_CREATE_SENSOR, + ], + NetatmoDeviceCategory.meter: [NETATMO_CREATE_SENSOR], + } + for module in home.modules.values(): + if not module.device_category: + continue + + for signal in netatmo_type_signal_map.get(module.device_category, []): + async_dispatcher_send( + self.hass, + signal, + NetatmoDevice( + self, + module, + home.entity_id, + signal_home, + ), + ) + if module.device_category is NetatmoDeviceCategory.weather: + async_dispatcher_send( + self.hass, + NETATMO_CREATE_WEATHER_SENSOR, + NetatmoDevice( + self, + module, + home.entity_id, + WEATHER, + ), + ) + + def setup_rooms(self, home: pyatmo.Home, signal_home: str) -> None: + """Set up rooms.""" + for room in home.rooms.values(): + if NetatmoDeviceCategory.climate in room.features: + async_dispatcher_send( + self.hass, + NETATMO_CREATE_CLIMATE, + NetatmoRoom( + self, + room, + home.entity_id, + signal_home, + ), + ) + + for module in room.modules.values(): + if module.device_category is NetatmoDeviceCategory.climate: + async_dispatcher_send( + self.hass, + NETATMO_CREATE_BATTERY, + NetatmoDevice( + self, + module, + room.entity_id, + signal_home, + ), + ) + + if "humidity" in room.features: + async_dispatcher_send( + self.hass, + NETATMO_CREATE_ROOM_SENSOR, + NetatmoRoom( + self, + room, + room.entity_id, + signal_home, + ), + ) + + def setup_climate_schedule_select( + self, home: pyatmo.Home, signal_home: str + ) -> None: + """Set up climate schedule per home.""" + if NetatmoDeviceCategory.climate in [ + next(iter(x)) for x in [room.features for room in home.rooms.values()] if x + ]: + self.hass.data[DOMAIN][DATA_SCHEDULES][home.entity_id] = self.account.homes[ + home.entity_id + ].schedules + + async_dispatcher_send( + self.hass, + NETATMO_CREATE_SELECT, + NetatmoHome( + self, + home, + home.entity_id, + signal_home, + ), + ) diff --git a/homeassistant/components/netatmo/device_trigger.py b/homeassistant/components/netatmo/device_trigger.py index 955671e3dc1..b037f45533f 100644 --- a/homeassistant/components/netatmo/device_trigger.py +++ b/homeassistant/components/netatmo/device_trigger.py @@ -31,10 +31,6 @@ from .const import ( DOMAIN, EVENT_TYPE_THERM_MODE, INDOOR_CAMERA_TRIGGERS, - MODEL_NACAMERA, - MODEL_NATHERM1, - MODEL_NOC, - MODEL_NRV, NETATMO_EVENT, OUTDOOR_CAMERA_TRIGGERS, ) @@ -42,10 +38,10 @@ from .const import ( CONF_SUBTYPE = "subtype" DEVICES = { - MODEL_NACAMERA: INDOOR_CAMERA_TRIGGERS, - MODEL_NOC: OUTDOOR_CAMERA_TRIGGERS, - MODEL_NATHERM1: CLIMATE_TRIGGERS, - MODEL_NRV: CLIMATE_TRIGGERS, + "NACamera": INDOOR_CAMERA_TRIGGERS, + "NOC": OUTDOOR_CAMERA_TRIGGERS, + "NATherm1": CLIMATE_TRIGGERS, + "NRV": CLIMATE_TRIGGERS, } SUBTYPES = { @@ -76,7 +72,7 @@ async def async_validate_trigger_config( device_registry = dr.async_get(hass) device = device_registry.async_get(config[CONF_DEVICE_ID]) - if not device: + if not device or device.model is None: raise InvalidDeviceAutomationConfig( f"Trigger invalid, device with ID {config[CONF_DEVICE_ID]} not found" ) diff --git a/homeassistant/components/netatmo/diagnostics.py b/homeassistant/components/netatmo/diagnostics.py index 6c82c7f1db7..cac9c695f19 100644 --- a/homeassistant/components/netatmo/diagnostics.py +++ b/homeassistant/components/netatmo/diagnostics.py @@ -6,7 +6,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from .const import DATA_HANDLER, DOMAIN -from .data_handler import CLIMATE_TOPOLOGY_CLASS_NAME, NetatmoDataHandler +from .data_handler import ACCOUNT, NetatmoDataHandler TO_REDACT = { "access_token", @@ -45,8 +45,8 @@ async def async_get_config_entry_diagnostics( TO_REDACT, ), "data": { - CLIMATE_TOPOLOGY_CLASS_NAME: async_redact_data( - getattr(data_handler.data[CLIMATE_TOPOLOGY_CLASS_NAME], "raw_data"), + ACCOUNT: async_redact_data( + getattr(data_handler.account, "raw_data"), TO_REDACT, ) }, diff --git a/homeassistant/components/netatmo/light.py b/homeassistant/components/netatmo/light.py index 6567ae770f2..b3e352eb7d8 100644 --- a/homeassistant/components/netatmo/light.py +++ b/homeassistant/components/netatmo/light.py @@ -4,25 +4,25 @@ from __future__ import annotations import logging from typing import Any, cast -import pyatmo +from pyatmo import modules as NaModules -from homeassistant.components.light import ColorMode, LightEntity +from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( - DATA_HANDLER, + CONF_URL_CONTROL, + CONF_URL_SECURITY, DOMAIN, EVENT_TYPE_LIGHT_MODE, - SIGNAL_NAME, - TYPE_SECURITY, + NETATMO_CREATE_CAMERA_LIGHT, + NETATMO_CREATE_LIGHT, WEBHOOK_LIGHT_MODE, WEBHOOK_PUSH_TYPE, ) -from .data_handler import CAMERA_DATA_CLASS_NAME, NetatmoDataHandler +from .data_handler import HOME, SIGNAL_NAME, NetatmoDevice from .netatmo_entity_base import NetatmoBase _LOGGER = logging.getLogger(__name__) @@ -32,66 +32,73 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the Netatmo camera light platform.""" - data_handler = hass.data[DOMAIN][entry.entry_id][DATA_HANDLER] - data_class = data_handler.data.get(CAMERA_DATA_CLASS_NAME) - if not data_class or data_class.raw_data == {}: - raise PlatformNotReady + @callback + def _create_camera_light_entity(netatmo_device: NetatmoDevice) -> None: + if not hasattr(netatmo_device.device, "floodlight"): + return - all_cameras = [] - for home in data_handler.data[CAMERA_DATA_CLASS_NAME].cameras.values(): - for camera in home.values(): - all_cameras.append(camera) + entity = NetatmoCameraLight(netatmo_device) + async_add_entities([entity]) - entities = [ - NetatmoLight( - data_handler, - camera["id"], - camera["type"], - camera["home_id"], + entry.async_on_unload( + async_dispatcher_connect( + hass, NETATMO_CREATE_CAMERA_LIGHT, _create_camera_light_entity ) - for camera in all_cameras - if camera["type"] == "NOC" - ] + ) - _LOGGER.debug("Adding camera lights %s", entities) - async_add_entities(entities, True) + @callback + def _create_entity(netatmo_device: NetatmoDevice) -> None: + if not hasattr(netatmo_device.device, "brightness"): + return + + entity = NetatmoLight(netatmo_device) + _LOGGER.debug("Adding light %s", entity) + async_add_entities([entity]) + + entry.async_on_unload( + async_dispatcher_connect(hass, NETATMO_CREATE_LIGHT, _create_entity) + ) -class NetatmoLight(NetatmoBase, LightEntity): +class NetatmoCameraLight(NetatmoBase, LightEntity): """Representation of a Netatmo Presence camera light.""" - _attr_color_mode = ColorMode.ONOFF _attr_has_entity_name = True - _attr_supported_color_modes = {ColorMode.ONOFF} def __init__( self, - data_handler: NetatmoDataHandler, - camera_id: str, - camera_type: str, - home_id: str, + netatmo_device: NetatmoDevice, ) -> None: """Initialize a Netatmo Presence camera light.""" LightEntity.__init__(self) - super().__init__(data_handler) + super().__init__(netatmo_device.data_handler) - self._publishers.append( - {"name": CAMERA_DATA_CLASS_NAME, SIGNAL_NAME: CAMERA_DATA_CLASS_NAME} - ) - self._id = camera_id - self._home_id = home_id - self._model = camera_type - self._netatmo_type = TYPE_SECURITY - self._device_name: str = self._data.get_camera(camera_id)["name"] + self._camera = cast(NaModules.NOC, netatmo_device.device) + self._id = self._camera.entity_id + self._home_id = self._camera.home.entity_id + self._device_name = self._camera.name + self._model = self._camera.device_type + self._config_url = CONF_URL_SECURITY self._is_on = False self._attr_unique_id = f"{self._id}-light" + self._signal_name = f"{HOME}-{self._home_id}" + self._publishers.extend( + [ + { + "name": HOME, + "home_id": self._camera.home.entity_id, + SIGNAL_NAME: self._signal_name, + }, + ] + ) + async def async_added_to_hass(self) -> None: """Entity created.""" await super().async_added_to_hass() - self.data_handler.config_entry.async_on_unload( + self.async_on_remove( async_dispatcher_connect( self.hass, f"signal-{DOMAIN}-webhook-{EVENT_TYPE_LIGHT_MODE}", @@ -117,14 +124,6 @@ class NetatmoLight(NetatmoBase, LightEntity): self.async_write_ha_state() return - @property - def _data(self) -> pyatmo.AsyncCameraData: - """Return data for this entity.""" - return cast( - pyatmo.AsyncCameraData, - self.data_handler.data[self._publishers[0]["name"]], - ) - @property def available(self) -> bool: """If the webhook is not established, mark as unavailable.""" @@ -138,22 +137,79 @@ class NetatmoLight(NetatmoBase, LightEntity): async def async_turn_on(self, **kwargs: Any) -> None: """Turn camera floodlight on.""" _LOGGER.debug("Turn camera '%s' on", self.name) - await self._data.async_set_state( - home_id=self._home_id, - camera_id=self._id, - floodlight="on", - ) + await self._camera.async_floodlight_on() async def async_turn_off(self, **kwargs: Any) -> None: """Turn camera floodlight into auto mode.""" _LOGGER.debug("Turn camera '%s' to auto mode", self.name) - await self._data.async_set_state( - home_id=self._home_id, - camera_id=self._id, - floodlight="auto", - ) + await self._camera.async_floodlight_auto() @callback def async_update_callback(self) -> None: """Update the entity's state.""" - self._is_on = bool(self._data.get_light_state(self._id) == "on") + self._is_on = bool(self._camera.floodlight == "on") + + +class NetatmoLight(NetatmoBase, LightEntity): + """Representation of a dimmable light by Legrand/BTicino.""" + + def __init__( + self, + netatmo_device: NetatmoDevice, + ) -> None: + """Initialize a Netatmo light.""" + super().__init__(netatmo_device.data_handler) + + self._dimmer = cast(NaModules.NLFN, netatmo_device.device) + self._id = self._dimmer.entity_id + self._home_id = self._dimmer.home.entity_id + self._device_name = self._dimmer.name + self._attr_name = f"{self._device_name}" + self._model = self._dimmer.device_type + self._config_url = CONF_URL_CONTROL + self._attr_brightness = 0 + self._attr_unique_id = f"{self._id}-light" + + self._attr_supported_color_modes: set[str] = set() + + if not self._attr_supported_color_modes and self._dimmer.brightness is not None: + self._attr_supported_color_modes.add(ColorMode.BRIGHTNESS) + + self._signal_name = f"{HOME}-{self._home_id}" + self._publishers.extend( + [ + { + "name": HOME, + "home_id": self._dimmer.home.entity_id, + SIGNAL_NAME: self._signal_name, + }, + ] + ) + + @property + def is_on(self) -> bool: + """Return true if light is on.""" + return self._dimmer.on is True + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn light on.""" + _LOGGER.debug("Turn light '%s' on", self.name) + if ATTR_BRIGHTNESS in kwargs: + await self._dimmer.async_set_brightness(kwargs[ATTR_BRIGHTNESS]) + + else: + await self._dimmer.async_on() + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn light off.""" + _LOGGER.debug("Turn light '%s' off", self.name) + await self._dimmer.async_off() + + @callback + def async_update_callback(self) -> None: + """Update the entity's state.""" + if self._dimmer.brightness is not None: + # Netatmo uses a range of [0, 100] to control brightness + self._attr_brightness = round((self._dimmer.brightness / 100) * 255) + else: + self._attr_brightness = None diff --git a/homeassistant/components/netatmo/manifest.json b/homeassistant/components/netatmo/manifest.json index 2081f9bd274..74d34056241 100644 --- a/homeassistant/components/netatmo/manifest.json +++ b/homeassistant/components/netatmo/manifest.json @@ -2,7 +2,7 @@ "domain": "netatmo", "name": "Netatmo", "documentation": "https://www.home-assistant.io/integrations/netatmo", - "requirements": ["pyatmo==6.2.4"], + "requirements": ["pyatmo==7.1.0"], "after_dependencies": ["cloud", "media_source"], "dependencies": ["application_credentials", "webhook"], "codeowners": ["@cgtobi"], @@ -11,5 +11,11 @@ "models": ["Healty Home Coach", "Netatmo Relay", "Presence", "Welcome"] }, "iot_class": "cloud_polling", - "loggers": ["pyatmo"] + "loggers": ["pyatmo"], + "supported_brands": { + "legrand": "Legrand", + "bubendorff": "Bubendorff", + "smarther": "Smarther", + "bticino": "BTicino" + } } diff --git a/homeassistant/components/netatmo/media_source.py b/homeassistant/components/netatmo/media_source.py index f753a1163a5..58bf2f93c96 100644 --- a/homeassistant/components/netatmo/media_source.py +++ b/homeassistant/components/netatmo/media_source.py @@ -5,12 +5,7 @@ import datetime as dt import logging import re -from homeassistant.components.media_player.const import ( - MEDIA_CLASS_DIRECTORY, - MEDIA_CLASS_VIDEO, - MEDIA_TYPE_VIDEO, -) -from homeassistant.components.media_player.errors import BrowseError +from homeassistant.components.media_player import BrowseError, MediaClass, MediaType from homeassistant.components.media_source.error import MediaSourceError, Unresolvable from homeassistant.components.media_source.models import ( BrowseMediaSource, @@ -77,21 +72,13 @@ class NetatmoSource(MediaSource): self, source: str, camera_id: str, event_id: int | None = None ) -> BrowseMediaSource: if event_id and event_id in self.events[camera_id]: - created = dt.datetime.fromtimestamp(event_id) - if self.events[camera_id][event_id]["type"] == "outdoor": - thumbnail = ( - self.events[camera_id][event_id]["event_list"][0] - .get("snapshot", {}) - .get("url") - ) - message = remove_html_tags( - self.events[camera_id][event_id]["event_list"][0]["message"] - ) - else: - thumbnail = ( - self.events[camera_id][event_id].get("snapshot", {}).get("url") - ) - message = remove_html_tags(self.events[camera_id][event_id]["message"]) + created = dt.datetime.fromtimestamp( + self.events[camera_id][event_id]["event_time"] + ) + thumbnail = self.events[camera_id][event_id].get("snapshot", {}).get("url") + message = remove_html_tags( + self.events[camera_id][event_id].get("message", "") + ) title = f"{created} - {message}" else: title = self.hass.data[DOMAIN][DATA_CAMERAS].get(camera_id, MANUFACTURER) @@ -102,13 +89,13 @@ class NetatmoSource(MediaSource): else: path = f"{source}/{camera_id}" - media_class = MEDIA_CLASS_DIRECTORY if event_id is None else MEDIA_CLASS_VIDEO + media_class = MediaClass.DIRECTORY if event_id is None else MediaClass.VIDEO media = BrowseMediaSource( domain=DOMAIN, identifier=path, media_class=media_class, - media_content_type=MEDIA_TYPE_VIDEO, + media_content_type=MediaType.VIDEO, title=title, can_play=bool( event_id and self.events[camera_id][event_id].get("media_url") diff --git a/homeassistant/components/netatmo/netatmo_entity_base.py b/homeassistant/components/netatmo/netatmo_entity_base.py index e8a346ccd84..081d06f5d4f 100644 --- a/homeassistant/components/netatmo/netatmo_entity_base.py +++ b/homeassistant/components/netatmo/netatmo_entity_base.py @@ -3,20 +3,18 @@ from __future__ import annotations from typing import Any +from pyatmo.modules.device_types import ( + DEVICE_DESCRIPTION_MAP, + DeviceType as NetatmoDeviceType, +) + from homeassistant.const import ATTR_ATTRIBUTION from homeassistant.core import callback from homeassistant.helpers import device_registry as dr from homeassistant.helpers.entity import DeviceInfo, Entity -from .const import ( - DATA_DEVICE_IDS, - DEFAULT_ATTRIBUTION, - DOMAIN, - MANUFACTURER, - MODELS, - SIGNAL_NAME, -) -from .data_handler import PUBLICDATA_DATA_CLASS_NAME, NetatmoDataHandler +from .const import DATA_DEVICE_IDS, DEFAULT_ATTRIBUTION, DOMAIN, SIGNAL_NAME +from .data_handler import PUBLIC, NetatmoDataHandler class NetatmoBase(Entity): @@ -30,38 +28,38 @@ class NetatmoBase(Entity): self._device_name: str = "" self._id: str = "" self._model: str = "" - self._netatmo_type: str = "" + self._config_url: str = "" self._attr_name = None self._attr_unique_id = None self._attr_extra_state_attributes = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION} async def async_added_to_hass(self) -> None: """Entity created.""" - for data_class in self._publishers: - signal_name = data_class[SIGNAL_NAME] + for publisher in self._publishers: + signal_name = publisher[SIGNAL_NAME] - if "home_id" in data_class: + if "home_id" in publisher: await self.data_handler.subscribe( - data_class["name"], + publisher["name"], signal_name, self.async_update_callback, - home_id=data_class["home_id"], + home_id=publisher["home_id"], ) - elif data_class["name"] == PUBLICDATA_DATA_CLASS_NAME: + elif publisher["name"] == PUBLIC: await self.data_handler.subscribe( - data_class["name"], + publisher["name"], signal_name, self.async_update_callback, - lat_ne=data_class["lat_ne"], - lon_ne=data_class["lon_ne"], - lat_sw=data_class["lat_sw"], - lon_sw=data_class["lon_sw"], + lat_ne=publisher["lat_ne"], + lon_ne=publisher["lon_ne"], + lat_sw=publisher["lat_sw"], + lon_sw=publisher["lon_sw"], ) else: await self.data_handler.subscribe( - data_class["name"], signal_name, self.async_update_callback + publisher["name"], signal_name, self.async_update_callback ) for sub in self.data_handler.publisher[signal_name].subscriptions: @@ -78,9 +76,9 @@ class NetatmoBase(Entity): """Run when entity will be removed from hass.""" await super().async_will_remove_from_hass() - for data_class in self._publishers: + for publisher in self._publishers: await self.data_handler.unsubscribe( - data_class[SIGNAL_NAME], self.async_update_callback + publisher[SIGNAL_NAME], self.async_update_callback ) @callback @@ -91,10 +89,13 @@ class NetatmoBase(Entity): @property def device_info(self) -> DeviceInfo: """Return the device info for the sensor.""" + manufacturer, model = DEVICE_DESCRIPTION_MAP[ + getattr(NetatmoDeviceType, self._model) + ] return DeviceInfo( - configuration_url=f"https://my.netatmo.com/app/{self._netatmo_type}", + configuration_url=self._config_url, identifiers={(DOMAIN, self._id)}, name=self._device_name, - manufacturer=MANUFACTURER, - model=MODELS[self._model], + manufacturer=manufacturer, + model=model, ) diff --git a/homeassistant/components/netatmo/select.py b/homeassistant/components/netatmo/select.py index 62e6ef25969..3651ae05e88 100644 --- a/homeassistant/components/netatmo/select.py +++ b/homeassistant/components/netatmo/select.py @@ -3,29 +3,20 @@ from __future__ import annotations import logging -import pyatmo - from homeassistant.components.select import SelectEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( - DATA_HANDLER, + CONF_URL_ENERGY, DATA_SCHEDULES, DOMAIN, EVENT_TYPE_SCHEDULE, - MANUFACTURER, - SIGNAL_NAME, - TYPE_ENERGY, -) -from .data_handler import ( - CLIMATE_STATE_CLASS_NAME, - CLIMATE_TOPOLOGY_CLASS_NAME, - NetatmoDataHandler, + NETATMO_CREATE_SELECT, ) +from .data_handler import HOME, SIGNAL_NAME, NetatmoHome from .netatmo_entity_base import NetatmoBase _LOGGER = logging.getLogger(__name__) @@ -35,100 +26,66 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the Netatmo energy platform schedule selector.""" - data_handler = hass.data[DOMAIN][entry.entry_id][DATA_HANDLER] - climate_topology = data_handler.data.get(CLIMATE_TOPOLOGY_CLASS_NAME) + @callback + def _create_entity(netatmo_home: NetatmoHome) -> None: + entity = NetatmoScheduleSelect(netatmo_home) + async_add_entities([entity]) - if not climate_topology or climate_topology.raw_data == {}: - raise PlatformNotReady - - entities = [] - for home_id in climate_topology.home_ids: - signal_name = f"{CLIMATE_STATE_CLASS_NAME}-{home_id}" - - await data_handler.subscribe( - CLIMATE_STATE_CLASS_NAME, signal_name, None, home_id=home_id - ) - - if (climate_state := data_handler.data[signal_name]) is None: - continue - - climate_topology.register_handler(home_id, climate_state.process_topology) - - hass.data[DOMAIN][DATA_SCHEDULES][home_id] = climate_state.homes[ - home_id - ].schedules - - entities = [ - NetatmoScheduleSelect( - data_handler, - home_id, - [schedule.name for schedule in schedules.values()], - ) - for home_id, schedules in hass.data[DOMAIN][DATA_SCHEDULES].items() - if schedules - ] - - _LOGGER.debug("Adding climate schedule select entities %s", entities) - async_add_entities(entities, True) + entry.async_on_unload( + async_dispatcher_connect(hass, NETATMO_CREATE_SELECT, _create_entity) + ) class NetatmoScheduleSelect(NetatmoBase, SelectEntity): """Representation a Netatmo thermostat schedule selector.""" def __init__( - self, data_handler: NetatmoDataHandler, home_id: str, options: list + self, + netatmo_home: NetatmoHome, ) -> None: """Initialize the select entity.""" SelectEntity.__init__(self) - super().__init__(data_handler) + super().__init__(netatmo_home.data_handler) - self._home_id = home_id - - self._climate_state_class = f"{CLIMATE_STATE_CLASS_NAME}-{self._home_id}" - self._climate_state: pyatmo.AsyncClimate = data_handler.data[ - self._climate_state_class - ] - - self._home = self._climate_state.homes[self._home_id] + self._home = netatmo_home.home + self._home_id = self._home.entity_id + self._signal_name = netatmo_home.signal_name self._publishers.extend( [ { - "name": CLIMATE_TOPOLOGY_CLASS_NAME, - SIGNAL_NAME: CLIMATE_TOPOLOGY_CLASS_NAME, - }, - { - "name": CLIMATE_STATE_CLASS_NAME, - "home_id": self._home_id, - SIGNAL_NAME: self._climate_state_class, + "name": HOME, + "home_id": self._home.entity_id, + SIGNAL_NAME: self._signal_name, }, ] ) self._device_name = self._home.name - self._attr_name = f"{MANUFACTURER} {self._device_name}" + self._attr_name = f"{self._device_name}" self._model: str = "NATherm1" - self._netatmo_type = TYPE_ENERGY + self._config_url = CONF_URL_ENERGY self._attr_unique_id = f"{self._home_id}-schedule-select" self._attr_current_option = getattr(self._home.get_selected_schedule(), "name") - self._attr_options = options + self._attr_options = [ + schedule.name for schedule in self._home.schedules.values() + ] async def async_added_to_hass(self) -> None: """Entity created.""" await super().async_added_to_hass() - for event_type in (EVENT_TYPE_SCHEDULE,): - self.data_handler.config_entry.async_on_unload( - async_dispatcher_connect( - self.hass, - f"signal-{DOMAIN}-webhook-{event_type}", - self.handle_event, - ) + self.async_on_remove( + async_dispatcher_connect( + self.hass, + f"signal-{DOMAIN}-webhook-{EVENT_TYPE_SCHEDULE}", + self.handle_event, ) + ) @callback def handle_event(self, event: dict) -> None: @@ -160,7 +117,7 @@ class NetatmoScheduleSelect(NetatmoBase, SelectEntity): option, sid, ) - await self._climate_state.async_switch_home_schedule(schedule_id=sid) + await self._home.async_switch_schedule(schedule_id=sid) break @callback @@ -169,8 +126,5 @@ class NetatmoScheduleSelect(NetatmoBase, SelectEntity): self._attr_current_option = getattr(self._home.get_selected_schedule(), "name") self.hass.data[DOMAIN][DATA_SCHEDULES][self._home_id] = self._home.schedules self._attr_options = [ - schedule.name - for schedule in self.hass.data[DOMAIN][DATA_SCHEDULES][ - self._home_id - ].values() + schedule.name for schedule in self._home.schedules.values() ] diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index bec9af96442..65ac610ef5d 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -1,9 +1,9 @@ -"""Support for the Netatmo Weather Service.""" +"""Support for the Netatmo sensors.""" from __future__ import annotations from dataclasses import dataclass import logging -from typing import NamedTuple, cast +from typing import cast import pyatmo @@ -21,15 +21,15 @@ from homeassistant.const import ( DEGREE, LENGTH_MILLIMETERS, PERCENTAGE, + POWER_WATT, PRESSURE_MBAR, - SIGNAL_STRENGTH_DECIBELS_MILLIWATT, SOUND_PRESSURE_DB, SPEED_KILOMETERS_PER_HOUR, TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.device_registry import async_entries_for_config_entry from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, @@ -38,20 +38,18 @@ from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( + CONF_URL_ENERGY, + CONF_URL_WEATHER, CONF_WEATHER_AREAS, DATA_HANDLER, DOMAIN, NETATMO_CREATE_BATTERY, + NETATMO_CREATE_ROOM_SENSOR, + NETATMO_CREATE_SENSOR, + NETATMO_CREATE_WEATHER_SENSOR, SIGNAL_NAME, - TYPE_WEATHER, -) -from .data_handler import ( - HOMECOACH_DATA_CLASS_NAME, - PUBLICDATA_DATA_CLASS_NAME, - WEATHERSTATION_DATA_CLASS_NAME, - NetatmoDataHandler, - NetatmoDevice, ) +from .data_handler import HOME, PUBLIC, NetatmoDataHandler, NetatmoDevice, NetatmoRoom from .helper import NetatmoArea from .netatmo_entity_base import NetatmoBase @@ -62,10 +60,12 @@ SUPPORTED_PUBLIC_SENSOR_TYPES: tuple[str, ...] = ( "pressure", "humidity", "rain", - "windstrength", - "guststrength", + "wind_strength", + "gust_strength", "sum_rain_1", "sum_rain_24", + "wind_angle", + "gust_angle", ) @@ -85,7 +85,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( NetatmoSensorEntityDescription( key="temperature", name="Temperature", - netatmo_name="Temperature", + netatmo_name="temperature", entity_registry_enabled_default=True, native_unit_of_measurement=TEMP_CELSIUS, state_class=SensorStateClass.MEASUREMENT, @@ -101,7 +101,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( NetatmoSensorEntityDescription( key="co2", name="CO2", - netatmo_name="CO2", + netatmo_name="co2", native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, entity_registry_enabled_default=True, state_class=SensorStateClass.MEASUREMENT, @@ -110,7 +110,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( NetatmoSensorEntityDescription( key="pressure", name="Pressure", - netatmo_name="Pressure", + netatmo_name="pressure", entity_registry_enabled_default=True, native_unit_of_measurement=PRESSURE_MBAR, state_class=SensorStateClass.MEASUREMENT, @@ -126,7 +126,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( NetatmoSensorEntityDescription( key="noise", name="Noise", - netatmo_name="Noise", + netatmo_name="noise", entity_registry_enabled_default=True, native_unit_of_measurement=SOUND_PRESSURE_DB, icon="mdi:volume-high", @@ -135,7 +135,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( NetatmoSensorEntityDescription( key="humidity", name="Humidity", - netatmo_name="Humidity", + netatmo_name="humidity", entity_registry_enabled_default=True, native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, @@ -144,7 +144,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( NetatmoSensorEntityDescription( key="rain", name="Rain", - netatmo_name="Rain", + netatmo_name="rain", entity_registry_enabled_default=True, native_unit_of_measurement=LENGTH_MILLIMETERS, state_class=SensorStateClass.MEASUREMENT, @@ -156,7 +156,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( netatmo_name="sum_rain_1", entity_registry_enabled_default=False, native_unit_of_measurement=LENGTH_MILLIMETERS, - state_class=SensorStateClass.TOTAL_INCREASING, + state_class=SensorStateClass.TOTAL, icon="mdi:weather-rainy", ), NetatmoSensorEntityDescription( @@ -171,7 +171,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( NetatmoSensorEntityDescription( key="battery_percent", name="Battery Percent", - netatmo_name="battery_percent", + netatmo_name="battery", entity_registry_enabled_default=True, entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement=PERCENTAGE, @@ -181,14 +181,14 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( NetatmoSensorEntityDescription( key="windangle", name="Direction", - netatmo_name="WindAngle", + netatmo_name="wind_direction", entity_registry_enabled_default=True, icon="mdi:compass-outline", ), NetatmoSensorEntityDescription( key="windangle_value", name="Angle", - netatmo_name="WindAngle", + netatmo_name="wind_angle", entity_registry_enabled_default=False, native_unit_of_measurement=DEGREE, icon="mdi:compass-outline", @@ -197,23 +197,24 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( NetatmoSensorEntityDescription( key="windstrength", name="Wind Strength", - netatmo_name="WindStrength", + netatmo_name="wind_strength", entity_registry_enabled_default=True, native_unit_of_measurement=SPEED_KILOMETERS_PER_HOUR, + device_class=SensorDeviceClass.SPEED, icon="mdi:weather-windy", state_class=SensorStateClass.MEASUREMENT, ), NetatmoSensorEntityDescription( key="gustangle", name="Gust Direction", - netatmo_name="GustAngle", + netatmo_name="gust_direction", entity_registry_enabled_default=False, icon="mdi:compass-outline", ), NetatmoSensorEntityDescription( key="gustangle_value", name="Gust Angle", - netatmo_name="GustAngle", + netatmo_name="gust_angle", entity_registry_enabled_default=False, native_unit_of_measurement=DEGREE, icon="mdi:compass-outline", @@ -222,9 +223,10 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( NetatmoSensorEntityDescription( key="guststrength", name="Gust Strength", - netatmo_name="GustStrength", + netatmo_name="gust_strength", entity_registry_enabled_default=False, native_unit_of_measurement=SPEED_KILOMETERS_PER_HOUR, + device_class=SensorDeviceClass.SPEED, icon="mdi:weather-windy", state_class=SensorStateClass.MEASUREMENT, ), @@ -239,39 +241,19 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( NetatmoSensorEntityDescription( key="rf_status", name="Radio", - netatmo_name="rf_status", + netatmo_name="rf_strength", entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, icon="mdi:signal", ), - NetatmoSensorEntityDescription( - key="rf_status_lvl", - name="Radio Level", - netatmo_name="rf_status", - entity_registry_enabled_default=False, - entity_category=EntityCategory.DIAGNOSTIC, - native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, - state_class=SensorStateClass.MEASUREMENT, - device_class=SensorDeviceClass.SIGNAL_STRENGTH, - ), NetatmoSensorEntityDescription( key="wifi_status", name="Wifi", - netatmo_name="wifi_status", + netatmo_name="wifi_strength", entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, icon="mdi:wifi", ), - NetatmoSensorEntityDescription( - key="wifi_status_lvl", - name="Wifi Level", - netatmo_name="wifi_status", - entity_registry_enabled_default=False, - entity_category=EntityCategory.DIAGNOSTIC, - native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, - state_class=SensorStateClass.MEASUREMENT, - device_class=SensorDeviceClass.SIGNAL_STRENGTH, - ), NetatmoSensorEntityDescription( key="health_idx", name="Health", @@ -279,136 +261,110 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( entity_registry_enabled_default=True, icon="mdi:cloud", ), + NetatmoSensorEntityDescription( + key="power", + name="Power", + netatmo_name="power", + entity_registry_enabled_default=True, + native_unit_of_measurement=POWER_WATT, + state_class=SensorStateClass.TOTAL, + device_class=SensorDeviceClass.POWER, + ), ) SENSOR_TYPES_KEYS = [desc.key for desc in SENSOR_TYPES] -MODULE_TYPE_OUTDOOR = "NAModule1" -MODULE_TYPE_WIND = "NAModule2" -MODULE_TYPE_RAIN = "NAModule3" -MODULE_TYPE_INDOOR = "NAModule4" - - -class BatteryData(NamedTuple): - """Metadata for a batter.""" - - full: int - high: int - medium: int - low: int - - -BATTERY_VALUES = { - MODULE_TYPE_WIND: BatteryData( - full=5590, - high=5180, - medium=4770, - low=4360, - ), - MODULE_TYPE_RAIN: BatteryData( - full=5500, - high=5000, - medium=4500, - low=4000, - ), - MODULE_TYPE_INDOOR: BatteryData( - full=5500, - high=5280, - medium=4920, - low=4560, - ), - MODULE_TYPE_OUTDOOR: BatteryData( - full=5500, - high=5000, - medium=4500, - low=4000, - ), -} - -PUBLIC = "public" +BATTERY_SENSOR_DESCRIPTION = NetatmoSensorEntityDescription( + key="battery", + name="Battery Percent", + netatmo_name="battery", + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.BATTERY, +) async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: - """Set up the Netatmo weather and homecoach platform.""" - data_handler = hass.data[DOMAIN][entry.entry_id][DATA_HANDLER] - platform_not_ready = True + """Set up the Netatmo sensor platform.""" - async def find_entities(data_class_name: str) -> list: - """Find all entities.""" - all_module_infos = {} - data = data_handler.data + @callback + def _create_battery_entity(netatmo_device: NetatmoDevice) -> None: + if not hasattr(netatmo_device.device, "battery"): + return + entity = NetatmoClimateBatterySensor(netatmo_device) + async_add_entities([entity]) - if data_class_name not in data: - return [] + entry.async_on_unload( + async_dispatcher_connect(hass, NETATMO_CREATE_BATTERY, _create_battery_entity) + ) - if data[data_class_name] is None: - return [] + @callback + def _create_weather_sensor_entity(netatmo_device: NetatmoDevice) -> None: + async_add_entities( + NetatmoWeatherSensor(netatmo_device, description) + for description in SENSOR_TYPES + if description.netatmo_name in netatmo_device.device.features + ) - data_class = data[data_class_name] + entry.async_on_unload( + async_dispatcher_connect( + hass, NETATMO_CREATE_WEATHER_SENSOR, _create_weather_sensor_entity + ) + ) - for station_id in data_class.stations: - for module_id in data_class.get_modules(station_id): - all_module_infos[module_id] = data_class.get_module(module_id) - - all_module_infos[station_id] = data_class.get_station(station_id) - - entities = [] - for module in all_module_infos.values(): - if "_id" not in module: - _LOGGER.debug("Skipping module %s", module.get("module_name")) - continue - - conditions = [ - c.lower() - for c in data_class.get_monitored_conditions(module_id=module["_id"]) - if c.lower() in SENSOR_TYPES_KEYS + @callback + def _create_sensor_entity(netatmo_device: NetatmoDevice) -> None: + _LOGGER.debug( + "Adding %s sensor %s", + netatmo_device.device.device_category, + netatmo_device.device.name, + ) + async_add_entities( + [ + NetatmoSensor(netatmo_device, description) + for description in SENSOR_TYPES + if description.key in netatmo_device.device.features ] - for condition in conditions: - if f"{condition}_value" in SENSOR_TYPES_KEYS: - conditions.append(f"{condition}_value") - elif f"{condition}_lvl" in SENSOR_TYPES_KEYS: - conditions.append(f"{condition}_lvl") + ) - entities.extend( - [ - NetatmoSensor(data_handler, data_class_name, module, description) - for description in SENSOR_TYPES - if description.key in conditions - ] - ) + entry.async_on_unload( + async_dispatcher_connect(hass, NETATMO_CREATE_SENSOR, _create_sensor_entity) + ) - _LOGGER.debug("Adding weather sensors %s", entities) - return entities + @callback + def _create_room_sensor_entity(netatmo_device: NetatmoRoom) -> None: + async_add_entities( + NetatmoRoomSensor(netatmo_device, description) + for description in SENSOR_TYPES + if description.key in netatmo_device.room.features + ) - for data_class_name in ( - WEATHERSTATION_DATA_CLASS_NAME, - HOMECOACH_DATA_CLASS_NAME, - ): - data_class = data_handler.data.get(data_class_name) - - if data_class and data_class.raw_data: - platform_not_ready = False - - async_add_entities(await find_entities(data_class_name), True) + entry.async_on_unload( + async_dispatcher_connect( + hass, NETATMO_CREATE_ROOM_SENSOR, _create_room_sensor_entity + ) + ) device_registry = dr.async_get(hass) + data_handler = hass.data[DOMAIN][entry.entry_id][DATA_HANDLER] async def add_public_entities(update: bool = True) -> None: """Retrieve Netatmo public weather entities.""" entities = { device.name: device.id - for device in dr.async_entries_for_config_entry( + for device in async_entries_for_config_entry( device_registry, entry.entry_id ) - if device.model == "Public Weather stations" + if device.model == "Public Weather station" } new_entities = [] for area in [ NetatmoArea(**i) for i in entry.options.get(CONF_WEATHER_AREAS, {}).values() ]: - signal_name = f"{PUBLICDATA_DATA_CLASS_NAME}-{area.uuid}" + signal_name = f"{PUBLIC}-{area.uuid}" if area.area_name in entities: entities.pop(area.area_name) @@ -422,25 +378,21 @@ async def async_setup_entry( continue await data_handler.subscribe( - PUBLICDATA_DATA_CLASS_NAME, + PUBLIC, signal_name, None, lat_ne=area.lat_ne, lon_ne=area.lon_ne, lat_sw=area.lat_sw, lon_sw=area.lon_sw, + area_id=str(area.uuid), ) - data_class = data_handler.data.get(signal_name) - - if data_class and data_class.raw_data: - nonlocal platform_not_ready - platform_not_ready = False new_entities.extend( [ NetatmoPublicSensor(data_handler, area, description) for description in SENSOR_TYPES - if description.key in SUPPORTED_PUBLIC_SENSOR_TYPES + if description.netatmo_name in SUPPORTED_PUBLIC_SENSOR_TYPES ] ) @@ -454,68 +406,56 @@ async def async_setup_entry( hass, f"signal-{DOMAIN}-public-update-{entry.entry_id}", add_public_entities ) - @callback - def _create_entity(netatmo_device: NetatmoDevice) -> None: - entity = NetatmoClimateBatterySensor(netatmo_device) - _LOGGER.debug("Adding climate battery sensor %s", entity) - async_add_entities([entity]) - - entry.async_on_unload( - async_dispatcher_connect(hass, NETATMO_CREATE_BATTERY, _create_entity) - ) - await add_public_entities(False) - if platform_not_ready: - raise PlatformNotReady +class NetatmoWeatherSensor(NetatmoBase, SensorEntity): + """Implementation of a Netatmo weather/home coach sensor.""" -class NetatmoSensor(NetatmoBase, SensorEntity): - """Implementation of a Netatmo sensor.""" - + _attr_has_entity_name = True entity_description: NetatmoSensorEntityDescription def __init__( self, - data_handler: NetatmoDataHandler, - data_class_name: str, - module_info: dict, + netatmo_device: NetatmoDevice, description: NetatmoSensorEntityDescription, ) -> None: """Initialize the sensor.""" - super().__init__(data_handler) + super().__init__(netatmo_device.data_handler) self.entity_description = description - self._publishers.append({"name": data_class_name, SIGNAL_NAME: data_class_name}) + self._module = netatmo_device.device + self._id = self._module.entity_id + self._station_id = ( + self._module.bridge if self._module.bridge is not None else self._id + ) + self._device_name = self._module.name + category = getattr(self._module.device_category, "name") + self._publishers.extend( + [ + { + "name": category, + SIGNAL_NAME: category, + }, + ] + ) - self._id = module_info["_id"] - self._station_id = module_info.get("main_device", self._id) - - station = self._data.get_station(self._station_id) - if not (device := self._data.get_module(self._id)): - # Assume it's a station if module can't be found - device = station - - if device["type"] in ("NHC", "NAMain"): - self._device_name = module_info["station_name"] - else: - self._device_name = ( - f"{station['station_name']} " - f"{module_info.get('module_name', device['type'])}" - ) - - self._attr_name = f"{self._device_name} {description.name}" - self._model = device["type"] - self._netatmo_type = TYPE_WEATHER + self._attr_name = f"{description.name}" + self._model = self._module.device_type + self._config_url = CONF_URL_WEATHER self._attr_unique_id = f"{self._id}-{description.key}" - @property - def _data(self) -> pyatmo.AsyncWeatherStationData: - """Return data for this entity.""" - return cast( - pyatmo.AsyncWeatherStationData, - self.data_handler.data[self._publishers[0]["name"]], - ) + if hasattr(self._module, "place"): + place = cast( + pyatmo.modules.base_class.Place, getattr(self._module, "place") + ) + if hasattr(place, "location") and place.location is not None: + self._attr_extra_state_attributes.update( + { + ATTR_LATITUDE: place.location.latitude, + ATTR_LONGITUDE: place.location.longitude, + } + ) @property def available(self) -> bool: @@ -525,46 +465,25 @@ class NetatmoSensor(NetatmoBase, SensorEntity): @callback def async_update_callback(self) -> None: """Update the entity's state.""" - data = self._data.get_last_data(station_id=self._station_id, exclude=3600).get( - self._id - ) - - if data is None: - if self.state: - _LOGGER.debug( - "No data found for %s - %s (%s)", - self.name, - self._device_name, - self._id, - ) - self._attr_native_value = None + if ( + state := getattr(self._module, self.entity_description.netatmo_name) + ) is None: return - try: - state = data[self.entity_description.netatmo_name] - if self.entity_description.key in {"temperature", "pressure", "sum_rain_1"}: - self._attr_native_value = round(state, 1) - elif self.entity_description.key in {"windangle_value", "gustangle_value"}: - self._attr_native_value = fix_angle(state) - elif self.entity_description.key in {"windangle", "gustangle"}: - self._attr_native_value = process_angle(fix_angle(state)) - elif self.entity_description.key == "rf_status": - self._attr_native_value = process_rf(state) - elif self.entity_description.key == "wifi_status": - self._attr_native_value = process_wifi(state) - elif self.entity_description.key == "health_idx": - self._attr_native_value = process_health(state) - else: - self._attr_native_value = state - except KeyError: - if self.state: - _LOGGER.debug( - "No %s data found for %s", - self.entity_description.key, - self._device_name, - ) - self._attr_native_value = None - return + if self.entity_description.netatmo_name in { + "temperature", + "pressure", + "sum_rain_1", + }: + self._attr_native_value = round(state, 1) + elif self.entity_description.netatmo_name == "rf_strength": + self._attr_native_value = process_rf(state) + elif self.entity_description.netatmo_name == "wifi_strength": + self._attr_native_value = process_wifi(state) + elif self.entity_description.netatmo_name == "health_idx": + self._attr_native_value = process_health(state) + else: + self._attr_native_value = state self.async_write_ha_state() @@ -580,24 +499,25 @@ class NetatmoClimateBatterySensor(NetatmoBase, SensorEntity): ) -> None: """Initialize the sensor.""" super().__init__(netatmo_device.data_handler) - self.entity_description = NetatmoSensorEntityDescription( - key="battery_percent", - name="Battery Percent", - netatmo_name="battery_percent", - entity_registry_enabled_default=True, - entity_category=EntityCategory.DIAGNOSTIC, - native_unit_of_measurement=PERCENTAGE, - state_class=SensorStateClass.MEASUREMENT, - device_class=SensorDeviceClass.BATTERY, + self.entity_description = BATTERY_SENSOR_DESCRIPTION + + self._module = cast(pyatmo.modules.NRV, netatmo_device.device) + self._id = netatmo_device.parent_id + + self._publishers.extend( + [ + { + "name": HOME, + "home_id": netatmo_device.device.home.entity_id, + SIGNAL_NAME: netatmo_device.signal_name, + }, + ] ) - self._module = netatmo_device.device - self._id = netatmo_device.parent_id self._attr_name = f"{self._module.name} {self.entity_description.name}" - - self._signal_name = netatmo_device.signal_name self._room_id = self._module.room_id self._model = getattr(self._module.device_type, "value") + self._config_url = CONF_URL_ENERGY self._attr_unique_id = ( f"{self._id}-{self._module.entity_id}-{self.entity_description.key}" @@ -613,70 +533,54 @@ class NetatmoClimateBatterySensor(NetatmoBase, SensorEntity): return self._attr_available = True - self._attr_native_value = self._process_battery_state() - - def _process_battery_state(self) -> int | None: - """Construct room status.""" - if battery_state := self._module.battery_state: - return process_battery_percentage(battery_state) - - return None + self._attr_native_value = self._module.battery -def process_battery_percentage(data: str) -> int: - """Process battery data and return percent (int) for display.""" - mapping = { - "max": 100, - "full": 90, - "high": 75, - "medium": 50, - "low": 25, - "very low": 10, - } - return mapping[data] +class NetatmoSensor(NetatmoBase, SensorEntity): + """Implementation of a Netatmo sensor.""" + entity_description: NetatmoSensorEntityDescription -def fix_angle(angle: int) -> int: - """Fix angle when value is negative.""" - if angle < 0: - return 360 + angle - return angle + def __init__( + self, + netatmo_device: NetatmoDevice, + description: NetatmoSensorEntityDescription, + ) -> None: + """Initialize the sensor.""" + super().__init__(netatmo_device.data_handler) + self.entity_description = description + self._module = netatmo_device.device + self._id = self._module.entity_id -def process_angle(angle: int) -> str: - """Process angle and return string for display.""" - if angle >= 330: - return "N" - if angle >= 300: - return "NW" - if angle >= 240: - return "W" - if angle >= 210: - return "SW" - if angle >= 150: - return "S" - if angle >= 120: - return "SE" - if angle >= 60: - return "E" - if angle >= 30: - return "NE" - return "N" + self._publishers.extend( + [ + { + "name": HOME, + "home_id": netatmo_device.device.home.entity_id, + SIGNAL_NAME: netatmo_device.signal_name, + }, + ] + ) + self._attr_name = f"{self._module.name} {self.entity_description.name}" + self._room_id = self._module.room_id + self._model = getattr(self._module.device_type, "value") + self._config_url = CONF_URL_ENERGY -def process_battery(data: int, model: str) -> str: - """Process battery data and return string for display.""" - battery_data = BATTERY_VALUES[model] + self._attr_unique_id = ( + f"{self._id}-{self._module.entity_id}-{self.entity_description.key}" + ) - if data >= battery_data.full: - return "Full" - if data >= battery_data.high: - return "High" - if data >= battery_data.medium: - return "Medium" - if data >= battery_data.low: - return "Low" - return "Very Low" + @callback + def async_update_callback(self) -> None: + """Update the entity's state.""" + if (state := getattr(self._module, self.entity_description.key)) is None: + return + + self._attr_native_value = state + + self.async_write_ha_state() def process_health(health: int) -> str: @@ -714,9 +618,57 @@ def process_wifi(strength: int) -> str: return "Full" +class NetatmoRoomSensor(NetatmoBase, SensorEntity): + """Implementation of a Netatmo room sensor.""" + + entity_description: NetatmoSensorEntityDescription + + def __init__( + self, + netatmo_room: NetatmoRoom, + description: NetatmoSensorEntityDescription, + ) -> None: + """Initialize the sensor.""" + super().__init__(netatmo_room.data_handler) + self.entity_description = description + + self._room = netatmo_room.room + self._id = self._room.entity_id + + self._publishers.extend( + [ + { + "name": HOME, + "home_id": netatmo_room.room.home.entity_id, + SIGNAL_NAME: netatmo_room.signal_name, + }, + ] + ) + + self._attr_name = f"{self._room.name} {self.entity_description.name}" + self._room_id = self._room.entity_id + self._model = f"{self._room.climate_type}" + self._config_url = CONF_URL_ENERGY + + self._attr_unique_id = ( + f"{self._id}-{self._room.entity_id}-{self.entity_description.key}" + ) + + @callback + def async_update_callback(self) -> None: + """Update the entity's state.""" + if (state := getattr(self._room, self.entity_description.key)) is None: + return + + self._attr_native_value = state + + self.async_write_ha_state() + + class NetatmoPublicSensor(NetatmoBase, SensorEntity): """Represent a single sensor in a Netatmo.""" + _attr_has_entity_name = True entity_description: NetatmoSensorEntityDescription def __init__( @@ -729,11 +681,10 @@ class NetatmoPublicSensor(NetatmoBase, SensorEntity): super().__init__(data_handler) self.entity_description = description - self._signal_name = f"{PUBLICDATA_DATA_CLASS_NAME}-{area.uuid}" - + self._signal_name = f"{PUBLIC}-{area.uuid}" self._publishers.append( { - "name": PUBLICDATA_DATA_CLASS_NAME, + "name": PUBLIC, "lat_ne": area.lat_ne, "lon_ne": area.lon_ne, "lat_sw": area.lat_sw, @@ -743,12 +694,14 @@ class NetatmoPublicSensor(NetatmoBase, SensorEntity): } ) + self._station = data_handler.account.public_weather_areas[str(area.uuid)] + self.area = area self._mode = area.mode self._area_name = area.area_name self._id = self._area_name self._device_name = f"{self._area_name}" - self._attr_name = f"{self._device_name} {description.name}" + self._attr_name = f"{description.name}" self._show_on_map = area.show_on_map self._attr_unique_id = ( f"{self._device_name.replace(' ', '-')}-{description.key}" @@ -762,17 +715,12 @@ class NetatmoPublicSensor(NetatmoBase, SensorEntity): } ) - @property - def _data(self) -> pyatmo.AsyncPublicData: - """Return data for this entity.""" - return cast(pyatmo.AsyncPublicData, self.data_handler.data[self._signal_name]) - async def async_added_to_hass(self) -> None: """Entity created.""" await super().async_added_to_hass() assert self.device_info and "name" in self.device_info - self.data_handler.config_entry.async_on_unload( + self.async_on_remove( async_dispatcher_connect( self.hass, f"netatmo-config-{self.device_info['name']}", @@ -790,22 +738,11 @@ class NetatmoPublicSensor(NetatmoBase, SensorEntity): ) self.area = area - self._signal_name = f"{PUBLICDATA_DATA_CLASS_NAME}-{area.uuid}" - self._publishers = [ - { - "name": PUBLICDATA_DATA_CLASS_NAME, - "lat_ne": area.lat_ne, - "lon_ne": area.lon_ne, - "lat_sw": area.lat_sw, - "lon_sw": area.lon_sw, - "area_name": area.area_name, - SIGNAL_NAME: self._signal_name, - } - ] + self._signal_name = f"{PUBLIC}-{area.uuid}" self._mode = area.mode self._show_on_map = area.show_on_map await self.data_handler.subscribe( - PUBLICDATA_DATA_CLASS_NAME, + PUBLIC, self._signal_name, self.async_update_callback, lat_ne=area.lat_ne, @@ -819,22 +756,26 @@ class NetatmoPublicSensor(NetatmoBase, SensorEntity): """Update the entity's state.""" data = None - if self.entity_description.key == "temperature": - data = self._data.get_latest_temperatures() - elif self.entity_description.key == "pressure": - data = self._data.get_latest_pressures() - elif self.entity_description.key == "humidity": - data = self._data.get_latest_humidities() - elif self.entity_description.key == "rain": - data = self._data.get_latest_rain() - elif self.entity_description.key == "sum_rain_1": - data = self._data.get_60_min_rain() - elif self.entity_description.key == "sum_rain_24": - data = self._data.get_24_h_rain() - elif self.entity_description.key == "windstrength": - data = self._data.get_latest_wind_strengths() - elif self.entity_description.key == "guststrength": - data = self._data.get_latest_gust_strengths() + if self.entity_description.netatmo_name == "temperature": + data = self._station.get_latest_temperatures() + elif self.entity_description.netatmo_name == "pressure": + data = self._station.get_latest_pressures() + elif self.entity_description.netatmo_name == "humidity": + data = self._station.get_latest_humidities() + elif self.entity_description.netatmo_name == "rain": + data = self._station.get_latest_rain() + elif self.entity_description.netatmo_name == "sum_rain_1": + data = self._station.get_60_min_rain() + elif self.entity_description.netatmo_name == "sum_rain_24": + data = self._station.get_24_h_rain() + elif self.entity_description.netatmo_name == "wind_strength": + data = self._station.get_latest_wind_strengths() + elif self.entity_description.netatmo_name == "gust_strength": + data = self._station.get_latest_gust_strengths() + elif self.entity_description.netatmo_name == "wind_angle": + data = self._station.get_latest_wind_angles() + elif self.entity_description.netatmo_name == "gust_angle": + data = self._station.get_latest_gust_angles() if not data: if self.available: diff --git a/homeassistant/components/netatmo/switch.py b/homeassistant/components/netatmo/switch.py new file mode 100644 index 00000000000..338d073c205 --- /dev/null +++ b/homeassistant/components/netatmo/switch.py @@ -0,0 +1,83 @@ +"""Support for Netatmo/BTicino/Legrande switches.""" +from __future__ import annotations + +import logging +from typing import Any, cast + +from pyatmo import modules as NaModules + +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 .const import CONF_URL_CONTROL, NETATMO_CREATE_SWITCH +from .data_handler import HOME, SIGNAL_NAME, NetatmoDevice +from .netatmo_entity_base import NetatmoBase + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Netatmo switch platform.""" + + @callback + def _create_entity(netatmo_device: NetatmoDevice) -> None: + entity = NetatmoSwitch(netatmo_device) + _LOGGER.debug("Adding switch %s", entity) + async_add_entities([entity]) + + entry.async_on_unload( + async_dispatcher_connect(hass, NETATMO_CREATE_SWITCH, _create_entity) + ) + + +class NetatmoSwitch(NetatmoBase, SwitchEntity): + """Representation of a Netatmo switch device.""" + + def __init__( + self, + netatmo_device: NetatmoDevice, + ) -> None: + """Initialize the Netatmo device.""" + super().__init__(netatmo_device.data_handler) + + self._switch = cast(NaModules.Switch, netatmo_device.device) + + self._id = self._switch.entity_id + self._attr_name = self._device_name = self._switch.name + self._model = self._switch.device_type + self._config_url = CONF_URL_CONTROL + + self._home_id = self._switch.home.entity_id + + self._signal_name = f"{HOME}-{self._home_id}" + self._publishers.extend( + [ + { + "name": HOME, + "home_id": self._home_id, + SIGNAL_NAME: self._signal_name, + }, + ] + ) + self._attr_unique_id = f"{self._id}-{self._model}" + self._attr_is_on = self._switch.on + + @callback + def async_update_callback(self) -> None: + """Update the entity's state.""" + self._attr_is_on = self._switch.on + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the zone on.""" + await self._switch.async_on() + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the zone off.""" + await self._switch.async_off() diff --git a/homeassistant/components/netatmo/translations/bg.json b/homeassistant/components/netatmo/translations/bg.json index 30cbd4c7167..5e771b9224b 100644 --- a/homeassistant/components/netatmo/translations/bg.json +++ b/homeassistant/components/netatmo/translations/bg.json @@ -21,7 +21,8 @@ "step": { "public_weather": { "data": { - "area_name": "\u0418\u043c\u0435 \u043d\u0430 \u043e\u0431\u043b\u0430\u0441\u0442\u0442\u0430" + "area_name": "\u0418\u043c\u0435 \u043d\u0430 \u043e\u0431\u043b\u0430\u0441\u0442\u0442\u0430", + "show_on_map": "\u041f\u043e\u043a\u0430\u0437\u0432\u0430\u043d\u0435 \u043d\u0430 \u043a\u0430\u0440\u0442\u0430\u0442\u0430" } } } diff --git a/homeassistant/components/netatmo/translations/es.json b/homeassistant/components/netatmo/translations/es.json index 68f4160beb9..975995663da 100644 --- a/homeassistant/components/netatmo/translations/es.json +++ b/homeassistant/components/netatmo/translations/es.json @@ -4,7 +4,7 @@ "authorize_url_timeout": "Se agot\u00f3 el tiempo de espera para generar la URL de autorizaci\u00f3n.", "missing_configuration": "El componente no est\u00e1 configurado. Por favor, sigue la documentaci\u00f3n.", "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [revisa la secci\u00f3n de ayuda]({docs_url})", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente", "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." }, "create_entry": { diff --git a/homeassistant/components/netatmo/translations/ja.json b/homeassistant/components/netatmo/translations/ja.json index c60eb3dcd5e..671cc4da751 100644 --- a/homeassistant/components/netatmo/translations/ja.json +++ b/homeassistant/components/netatmo/translations/ja.json @@ -22,7 +22,7 @@ }, "device_automation": { "trigger_subtype": { - "away": "\u96e2\u5e2d(away)", + "away": "\u3042\u3061\u3089\u3078", "hg": "\u30d5\u30ed\u30b9\u30c8(frost)\u30ac\u30fc\u30c9", "schedule": "\u30b9\u30b1\u30b8\u30e5\u30fc\u30eb" }, diff --git a/homeassistant/components/netdata/sensor.py b/homeassistant/components/netdata/sensor.py index 97007ec076f..8c51a3fd9a6 100644 --- a/homeassistant/components/netdata/sensor.py +++ b/homeassistant/components/netdata/sensor.py @@ -140,11 +140,11 @@ class NetdataSensor(SensorEntity): return self._state @property - def available(self): + def available(self) -> bool: """Could the resource be accessed during the last update call.""" return self.netdata.available - async def async_update(self): + async def async_update(self) -> None: """Get the latest data from Netdata REST API.""" await self.netdata.async_update() resource_data = self.netdata.api.metrics.get(self._sensor) @@ -186,11 +186,11 @@ class NetdataAlarms(SensorEntity): return "mdi:crosshairs-question" @property - def available(self): + def available(self) -> bool: """Could the resource be accessed during the last update call.""" return self.netdata.available - async def async_update(self): + async def async_update(self) -> None: """Get the latest alarms from Netdata REST API.""" await self.netdata.async_update() alarms = self.netdata.api.alarms["alarms"] diff --git a/homeassistant/components/netgear/button.py b/homeassistant/components/netgear/button.py index e14600ff52b..15ee2652068 100644 --- a/homeassistant/components/netgear/button.py +++ b/homeassistant/components/netgear/button.py @@ -15,7 +15,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import DOMAIN, KEY_COORDINATOR, KEY_ROUTER -from .router import NetgearRouter, NetgearRouterEntity +from .router import NetgearRouter, NetgearRouterCoordinatorEntity @dataclass @@ -55,7 +55,7 @@ async def async_setup_entry( ) -class NetgearRouterButtonEntity(NetgearRouterEntity, ButtonEntity): +class NetgearRouterButtonEntity(NetgearRouterCoordinatorEntity, ButtonEntity): """Netgear Router button entity.""" entity_description: NetgearButtonEntityDescription diff --git a/homeassistant/components/netgear/router.py b/homeassistant/components/netgear/router.py index f69e88e83e2..c6384a44351 100644 --- a/homeassistant/components/netgear/router.py +++ b/homeassistant/components/netgear/router.py @@ -20,7 +20,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry as dr from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, format_mac -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -87,14 +87,14 @@ class NetgearRouter: ) self._consider_home = timedelta(seconds=consider_home_int) - self._api: Netgear = None - self._api_lock = asyncio.Lock() + self.api: Netgear = None + self.api_lock = asyncio.Lock() self.devices: dict[str, Any] = {} def _setup(self) -> bool: """Set up a Netgear router sync portion.""" - self._api = get_api( + self.api = get_api( self._password, self._host, self._username, @@ -102,7 +102,7 @@ class NetgearRouter: self._ssl, ) - self._info = self._api.get_info() + self._info = self.api.get_info() if self._info is None: return False @@ -130,7 +130,7 @@ class NetgearRouter: self.method_version = 2 if self.method_version == 2 and self.track_devices: - if not self._api.get_attached_devices_2(): + if not self.api.get_attached_devices_2(): _LOGGER.error( "Netgear Model '%s' in MODELS_V2 list, but failed to get attached devices using V2", self.model, @@ -141,7 +141,7 @@ class NetgearRouter: async def async_setup(self) -> bool: """Set up a Netgear router.""" - async with self._api_lock: + async with self.api_lock: if not await self.hass.async_add_executor_job(self._setup): return False @@ -175,14 +175,14 @@ class NetgearRouter: async def async_get_attached_devices(self) -> list: """Get the devices connected to the router.""" if self.method_version == 1: - async with self._api_lock: + async with self.api_lock: return await self.hass.async_add_executor_job( - self._api.get_attached_devices + self.api.get_attached_devices ) - async with self._api_lock: + async with self.api_lock: return await self.hass.async_add_executor_job( - self._api.get_attached_devices_2 + self.api.get_attached_devices_2 ) async def async_update_device_trackers(self, now=None) -> bool: @@ -221,57 +221,57 @@ class NetgearRouter: async def async_get_traffic_meter(self) -> dict[str, Any] | None: """Get the traffic meter data of the router.""" - async with self._api_lock: - return await self.hass.async_add_executor_job(self._api.get_traffic_meter) + async with self.api_lock: + return await self.hass.async_add_executor_job(self.api.get_traffic_meter) async def async_get_speed_test(self) -> dict[str, Any] | None: """Perform a speed test and get the results from the router.""" - async with self._api_lock: + async with self.api_lock: return await self.hass.async_add_executor_job( - self._api.get_new_speed_test_result + self.api.get_new_speed_test_result ) async def async_get_link_status(self) -> dict[str, Any] | None: """Check the ethernet link status of the router.""" - async with self._api_lock: - return await self.hass.async_add_executor_job(self._api.check_ethernet_link) + async with self.api_lock: + return await self.hass.async_add_executor_job(self.api.check_ethernet_link) async def async_allow_block_device(self, mac: str, allow_block: str) -> None: """Allow or block a device connected to the router.""" - async with self._api_lock: + async with self.api_lock: await self.hass.async_add_executor_job( - self._api.allow_block_device, mac, allow_block + self.api.allow_block_device, mac, allow_block ) async def async_get_utilization(self) -> dict[str, Any] | None: """Get the system information about utilization of the router.""" - async with self._api_lock: - return await self.hass.async_add_executor_job(self._api.get_system_info) + async with self.api_lock: + return await self.hass.async_add_executor_job(self.api.get_system_info) async def async_reboot(self) -> None: """Reboot the router.""" - async with self._api_lock: - await self.hass.async_add_executor_job(self._api.reboot) + async with self.api_lock: + await self.hass.async_add_executor_job(self.api.reboot) async def async_check_new_firmware(self) -> dict[str, Any] | None: """Check for new firmware of the router.""" - async with self._api_lock: - return await self.hass.async_add_executor_job(self._api.check_new_firmware) + async with self.api_lock: + return await self.hass.async_add_executor_job(self.api.check_new_firmware) async def async_update_new_firmware(self) -> None: """Update the router to the latest firmware.""" - async with self._api_lock: - await self.hass.async_add_executor_job(self._api.update_new_firmware) + async with self.api_lock: + await self.hass.async_add_executor_job(self.api.update_new_firmware) @property def port(self) -> int: """Port used by the API.""" - return self._api.port + return self.api.port @property def ssl(self) -> bool: """SSL used by the API.""" - return self._api.ssl + return self.api.ssl class NetgearBaseEntity(CoordinatorEntity): @@ -340,7 +340,7 @@ class NetgearDeviceEntity(NetgearBaseEntity): ) -class NetgearRouterEntity(CoordinatorEntity): +class NetgearRouterCoordinatorEntity(CoordinatorEntity): """Base class for a Netgear router entity.""" def __init__( @@ -379,3 +379,30 @@ class NetgearRouterEntity(CoordinatorEntity): return DeviceInfo( identifiers={(DOMAIN, self._router.unique_id)}, ) + + +class NetgearRouterEntity(Entity): + """Base class for a Netgear router entity without coordinator.""" + + def __init__(self, router: NetgearRouter) -> None: + """Initialize a Netgear device.""" + self._router = router + self._name = router.device_name + self._unique_id = router.serial_number + + @property + def unique_id(self) -> str: + """Return a unique ID.""" + return self._unique_id + + @property + def name(self) -> str: + """Return the name.""" + return self._name + + @property + def device_info(self) -> DeviceInfo: + """Return the device information.""" + return DeviceInfo( + identifiers={(DOMAIN, self._router.unique_id)}, + ) diff --git a/homeassistant/components/netgear/sensor.py b/homeassistant/components/netgear/sensor.py index 1ada340d1e1..c38142a3dc5 100644 --- a/homeassistant/components/netgear/sensor.py +++ b/homeassistant/components/netgear/sensor.py @@ -36,7 +36,7 @@ from .const import ( KEY_COORDINATOR_UTIL, KEY_ROUTER, ) -from .router import NetgearDeviceEntity, NetgearRouter, NetgearRouterEntity +from .router import NetgearDeviceEntity, NetgearRouter, NetgearRouterCoordinatorEntity _LOGGER = logging.getLogger(__name__) @@ -381,7 +381,7 @@ class NetgearSensorEntity(NetgearDeviceEntity, SensorEntity): self._state = self._device[self._attribute] -class NetgearRouterSensorEntity(NetgearRouterEntity, RestoreSensor): +class NetgearRouterSensorEntity(NetgearRouterCoordinatorEntity, RestoreSensor): """Representation of a device connected to a Netgear router.""" _attr_entity_registry_enabled_default = False diff --git a/homeassistant/components/netgear/switch.py b/homeassistant/components/netgear/switch.py index b38179ccb2a..6491ecf0abe 100644 --- a/homeassistant/components/netgear/switch.py +++ b/homeassistant/components/netgear/switch.py @@ -1,5 +1,9 @@ """Support for Netgear switches.""" +from collections.abc import Callable +from dataclasses import dataclass +from datetime import timedelta import logging +from typing import Any from pynetgear import ALLOW, BLOCK @@ -11,10 +15,11 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import DOMAIN, KEY_COORDINATOR, KEY_ROUTER -from .router import NetgearDeviceEntity, NetgearRouter +from .router import NetgearDeviceEntity, NetgearRouter, NetgearRouterEntity _LOGGER = logging.getLogger(__name__) +SCAN_INTERVAL = timedelta(seconds=300) SWITCH_TYPES = [ SwitchEntityDescription( @@ -26,11 +31,96 @@ SWITCH_TYPES = [ ] +@dataclass +class NetgearSwitchEntityDescriptionRequired: + """Required attributes of NetgearSwitchEntityDescription.""" + + update: Callable[[NetgearRouter], bool] + action: Callable[[NetgearRouter], bool] + + +@dataclass +class NetgearSwitchEntityDescription( + SwitchEntityDescription, NetgearSwitchEntityDescriptionRequired +): + """Class describing Netgear Switch entities.""" + + +ROUTER_SWITCH_TYPES = [ + NetgearSwitchEntityDescription( + key="access_control", + name="Access Control", + icon="mdi:block-helper", + entity_category=EntityCategory.CONFIG, + update=lambda router: router.api.get_block_device_enable_status, + action=lambda router: router.api.set_block_device_enable, + ), + NetgearSwitchEntityDescription( + key="traffic_meter", + name="Traffic Meter", + icon="mdi:wifi-arrow-up-down", + entity_category=EntityCategory.CONFIG, + update=lambda router: router.api.get_traffic_meter_enabled, + action=lambda router: router.api.enable_traffic_meter, + ), + NetgearSwitchEntityDescription( + key="parental_control", + name="Parental Control", + icon="mdi:account-child-outline", + entity_category=EntityCategory.CONFIG, + update=lambda router: router.api.get_parental_control_enable_status, + action=lambda router: router.api.enable_parental_control, + ), + NetgearSwitchEntityDescription( + key="qos", + name="Quality of Service", + icon="mdi:wifi-star", + entity_category=EntityCategory.CONFIG, + update=lambda router: router.api.get_qos_enable_status, + action=lambda router: router.api.set_qos_enable_status, + ), + NetgearSwitchEntityDescription( + key="2g_guest_wifi", + name="2.4G Guest Wifi", + icon="mdi:wifi", + entity_category=EntityCategory.CONFIG, + update=lambda router: router.api.get_2g_guest_access_enabled, + action=lambda router: router.api.set_2g_guest_access_enabled, + ), + NetgearSwitchEntityDescription( + key="5g_guest_wifi", + name="5G Guest Wifi", + icon="mdi:wifi", + entity_category=EntityCategory.CONFIG, + update=lambda router: router.api.get_5g_guest_access_enabled, + action=lambda router: router.api.set_5g_guest_access_enabled, + ), + NetgearSwitchEntityDescription( + key="smart_connect", + name="Smart Connect", + icon="mdi:wifi", + entity_category=EntityCategory.CONFIG, + update=lambda router: router.api.get_smart_connect_enabled, + action=lambda router: router.api.set_smart_connect_enabled, + ), +] + + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up switches for Netgear component.""" router = hass.data[DOMAIN][entry.entry_id][KEY_ROUTER] + + # Router entities + router_entities = [] + + for description in ROUTER_SWITCH_TYPES: + router_entities.append(NetgearRouterSwitchEntity(router, description)) + + async_add_entities(router_entities) + + # Entities per network device coordinator = hass.data[DOMAIN][entry.entry_id][KEY_COORDINATOR] tracked = set() @@ -79,20 +169,15 @@ class NetgearAllowBlock(NetgearDeviceEntity, SwitchEntity): self.entity_description = entity_description self._name = f"{self.get_device_name()} {self.entity_description.name}" self._unique_id = f"{self._mac}-{self.entity_description.key}" - self._state = None + self._attr_is_on = None self.async_update_device() - @property - def is_on(self): - """Return true if switch is on.""" - return self._state - - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" await self._router.async_allow_block_device(self._mac, ALLOW) await self.coordinator.async_request_refresh() - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the switch off.""" await self._router.async_allow_block_device(self._mac, BLOCK) await self.coordinator.async_request_refresh() @@ -103,6 +188,58 @@ class NetgearAllowBlock(NetgearDeviceEntity, SwitchEntity): self._device = self._router.devices[self._mac] self._active = self._device["active"] if self._device[self.entity_description.key] is None: - self._state = None + self._attr_is_on = None else: - self._state = self._device[self.entity_description.key] == "Allow" + self._attr_is_on = self._device[self.entity_description.key] == "Allow" + + +class NetgearRouterSwitchEntity(NetgearRouterEntity, SwitchEntity): + """Representation of a Netgear router switch.""" + + _attr_entity_registry_enabled_default = False + entity_description: NetgearSwitchEntityDescription + + def __init__( + self, + router: NetgearRouter, + entity_description: NetgearSwitchEntityDescription, + ) -> None: + """Initialize a Netgear device.""" + super().__init__(router) + self.entity_description = entity_description + self._name = f"{router.device_name} {entity_description.name}" + self._unique_id = f"{router.serial_number}-{entity_description.key}" + + self._attr_is_on = None + self._attr_available = False + + async def async_added_to_hass(self): + """Fetch state when entity is added.""" + await self.async_update() + await super().async_added_to_hass() + + async def async_update(self): + """Poll the state of the switch.""" + async with self._router.api_lock: + response = await self.hass.async_add_executor_job( + self.entity_description.update(self._router) + ) + if response is None: + self._attr_available = False + else: + self._attr_is_on = response + self._attr_available = True + + async def async_turn_on(self, **kwargs): + """Turn the switch on.""" + async with self._router.api_lock: + await self.hass.async_add_executor_job( + self.entity_description.action(self._router), True + ) + + async def async_turn_off(self, **kwargs): + """Turn the switch off.""" + async with self._router.api_lock: + await self.hass.async_add_executor_job( + self.entity_description.action(self._router), False + ) diff --git a/homeassistant/components/netgear/translations/ja.json b/homeassistant/components/netgear/translations/ja.json index 4402afcb92d..8cfa0de9e7e 100644 --- a/homeassistant/components/netgear/translations/ja.json +++ b/homeassistant/components/netgear/translations/ja.json @@ -13,7 +13,7 @@ "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "username": "\u30e6\u30fc\u30b6\u30fc\u540d(\u30aa\u30d7\u30b7\u30e7\u30f3)" }, - "description": "\u30c7\u30d5\u30a9\u30eb\u30c8\u306e\u30db\u30b9\u30c8: {host}\n\u30c7\u30d5\u30a9\u30eb\u30c8\u306e\u30dd\u30fc\u30c8: {port}\n\u30c7\u30d5\u30a9\u30eb\u30c8\u306e\u30e6\u30fc\u30b6\u30fc\u540d: {username}" + "description": "\u30c7\u30d5\u30a9\u30eb\u30c8\u306e\u30db\u30b9\u30c8: {host}\n\u30c7\u30d5\u30a9\u30eb\u30c8\u306e\u30e6\u30fc\u30b6\u30fc\u540d: {username}" } } }, diff --git a/homeassistant/components/netgear/update.py b/homeassistant/components/netgear/update.py index e913d488c8e..b0e9a26864b 100644 --- a/homeassistant/components/netgear/update.py +++ b/homeassistant/components/netgear/update.py @@ -15,7 +15,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import DOMAIN, KEY_COORDINATOR_FIRMWARE, KEY_ROUTER -from .router import NetgearRouter, NetgearRouterEntity +from .router import NetgearRouter, NetgearRouterCoordinatorEntity LOGGER = logging.getLogger(__name__) @@ -31,7 +31,7 @@ async def async_setup_entry( async_add_entities(entities) -class NetgearUpdateEntity(NetgearRouterEntity, UpdateEntity): +class NetgearUpdateEntity(NetgearRouterCoordinatorEntity, UpdateEntity): """Update entity for a Netgear device.""" _attr_device_class = UpdateDeviceClass.FIRMWARE diff --git a/homeassistant/components/netio/switch.py b/homeassistant/components/netio/switch.py index cfd736c9538..546aa07e22d 100644 --- a/homeassistant/components/netio/switch.py +++ b/homeassistant/components/netio/switch.py @@ -4,6 +4,7 @@ from __future__ import annotations from collections import namedtuple from datetime import timedelta import logging +from typing import Any from pynetio import Netio import voluptuous as vol @@ -148,15 +149,15 @@ class NetioSwitch(SwitchEntity): return self._name @property - def available(self): + def available(self) -> bool: """Return true if entity is available.""" return not hasattr(self, "telnet") - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn switch on.""" self._set(True) - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn switch off.""" self._set(False) @@ -172,6 +173,6 @@ class NetioSwitch(SwitchEntity): """Return the switch's status.""" return self.netio.states[int(self.outlet) - 1] - def update(self): + def update(self) -> None: """Update the state.""" self.netio.update() diff --git a/homeassistant/components/network/manifest.json b/homeassistant/components/network/manifest.json index 9f2fa7849f0..40712a40faf 100644 --- a/homeassistant/components/network/manifest.json +++ b/homeassistant/components/network/manifest.json @@ -6,5 +6,6 @@ "codeowners": ["@home-assistant/core"], "dependencies": ["websocket_api"], "quality_scale": "internal", - "iot_class": "local_push" + "iot_class": "local_push", + "integration_type": "system" } diff --git a/homeassistant/components/neurio_energy/sensor.py b/homeassistant/components/neurio_energy/sensor.py index 8270e89a33c..a1f6791fa5a 100644 --- a/homeassistant/components/neurio_energy/sensor.py +++ b/homeassistant/components/neurio_energy/sensor.py @@ -177,7 +177,7 @@ class NeurioEnergy(SensorEntity): """Icon to use in the frontend, if any.""" return ICON - def update(self): + def update(self) -> None: """Get the latest data, update state.""" self.update_sensor() diff --git a/homeassistant/components/nexia/climate.py b/homeassistant/components/nexia/climate.py index 33ad91e1561..81e6158a872 100644 --- a/homeassistant/components/nexia/climate.py +++ b/homeassistant/components/nexia/climate.py @@ -1,6 +1,8 @@ """Support for Nexia / Trane XL thermostats.""" from __future__ import annotations +from typing import Any + from nexia.const import ( HOLD_PERMANENT, HOLD_RESUME_SCHEDULE, @@ -18,12 +20,12 @@ from nexia.util import find_humidity_setpoint from nexia.zone import NexiaThermostatZone import voluptuous as vol -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( ATTR_HUMIDITY, ATTR_HVAC_MODE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, + ClimateEntity, ClimateEntityFeature, HVACAction, HVACMode, @@ -195,7 +197,7 @@ class NexiaZone(NexiaThermostatZoneEntity, ClimateEntity): """Return the fan setting.""" return self._thermostat.get_fan_mode() - async def async_set_fan_mode(self, fan_mode): + async def async_set_fan_mode(self, fan_mode: str) -> None: """Set new target fan mode.""" await self._thermostat.set_fan_mode(fan_mode) self._signal_thermostat_update() @@ -216,7 +218,7 @@ class NexiaZone(NexiaThermostatZoneEntity, ClimateEntity): """Preset that is active.""" return self._zone.get_preset() - async def async_set_humidity(self, humidity): + async def async_set_humidity(self, humidity: int) -> None: """Dehumidify target.""" if self._thermostat.has_dehumidify_support(): await self.async_set_dehumidify_setpoint(humidity) @@ -303,7 +305,7 @@ class NexiaZone(NexiaThermostatZoneEntity, ClimateEntity): return NEXIA_TO_HA_HVAC_MODE_MAP[mode] - async def async_set_temperature(self, **kwargs): + async def async_set_temperature(self, **kwargs: Any) -> None: """Set target temperature.""" new_heat_temp = kwargs.get(ATTR_TARGET_TEMP_LOW) new_cool_temp = kwargs.get(ATTR_TARGET_TEMP_HIGH) @@ -364,27 +366,27 @@ class NexiaZone(NexiaThermostatZoneEntity, ClimateEntity): attrs[ATTR_HUMIDIFY_SETPOINT] = humdify_setpoint return attrs - async def async_set_preset_mode(self, preset_mode: str): + async def async_set_preset_mode(self, preset_mode: str) -> None: """Set the preset mode.""" await self._zone.set_preset(preset_mode) self._signal_zone_update() - async def async_turn_aux_heat_off(self): + async def async_turn_aux_heat_off(self) -> None: """Turn Aux Heat off.""" await self._thermostat.set_emergency_heat(False) self._signal_thermostat_update() - async def async_turn_aux_heat_on(self): + async def async_turn_aux_heat_on(self) -> None: """Turn Aux Heat on.""" self._thermostat.set_emergency_heat(True) self._signal_thermostat_update() - async def async_turn_off(self): + async def async_turn_off(self) -> None: """Turn off the zone.""" await self.async_set_hvac_mode(OPERATION_MODE_OFF) self._signal_zone_update() - async def async_turn_on(self): + async def async_turn_on(self) -> None: """Turn on the zone.""" await self.async_set_hvac_mode(OPERATION_MODE_AUTO) self._signal_zone_update() diff --git a/homeassistant/components/nextbus/sensor.py b/homeassistant/components/nextbus/sensor.py index 12d43f32d07..5ab5d79caf7 100644 --- a/homeassistant/components/nextbus/sensor.py +++ b/homeassistant/components/nextbus/sensor.py @@ -169,7 +169,7 @@ class NextBusDepartureSensor(SensorEntity): """Return additional state attributes.""" return self._attributes - def update(self): + def update(self) -> None: """Update sensor with new departures times.""" # Note: using Multi because there is a bug with the single stop impl results = self._client.get_predictions_for_multi_stops( diff --git a/homeassistant/components/nextcloud/binary_sensor.py b/homeassistant/components/nextcloud/binary_sensor.py index fca0c7b44a7..e9d5b4a8d7f 100644 --- a/homeassistant/components/nextcloud/binary_sensor.py +++ b/homeassistant/components/nextcloud/binary_sensor.py @@ -53,6 +53,6 @@ class NextcloudBinarySensor(BinarySensorEntity): """Return the unique ID for this binary sensor.""" return f"{self.hass.data[DOMAIN]['instance']}#{self._name}" - def update(self): + def update(self) -> None: """Update the binary sensor.""" self._is_on = self.hass.data[DOMAIN][self._name] diff --git a/homeassistant/components/nextcloud/sensor.py b/homeassistant/components/nextcloud/sensor.py index e912bcdd806..31caa46028f 100644 --- a/homeassistant/components/nextcloud/sensor.py +++ b/homeassistant/components/nextcloud/sensor.py @@ -53,6 +53,6 @@ class NextcloudSensor(SensorEntity): """Return the unique ID for this sensor.""" return f"{self.hass.data[DOMAIN]['instance']}#{self._name}" - def update(self): + def update(self) -> None: """Update the sensor.""" self._state = self.hass.data[DOMAIN][self._name] diff --git a/homeassistant/components/nextdns/translations/bg.json b/homeassistant/components/nextdns/translations/bg.json new file mode 100644 index 00000000000..e476a498ed6 --- /dev/null +++ b/homeassistant/components/nextdns/translations/bg.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "\u0422\u043e\u0437\u0438 NextDNS \u043f\u0440\u043e\u0444\u0438\u043b \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_api_key": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d API \u043a\u043b\u044e\u0447", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "profiles": { + "data": { + "profile": "\u041f\u0440\u043e\u0444\u0438\u043b" + } + }, + "user": { + "data": { + "api_key": "API \u043a\u043b\u044e\u0447" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nextdns/translations/cs.json b/homeassistant/components/nextdns/translations/cs.json new file mode 100644 index 00000000000..849220d4ba6 --- /dev/null +++ b/homeassistant/components/nextdns/translations/cs.json @@ -0,0 +1,9 @@ +{ + "config": { + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_api_key": "Neplatn\u00fd kl\u00ed\u010d API", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nextdns/translations/id.json b/homeassistant/components/nextdns/translations/id.json index 39f87faf813..39b471b4053 100644 --- a/homeassistant/components/nextdns/translations/id.json +++ b/homeassistant/components/nextdns/translations/id.json @@ -20,5 +20,10 @@ } } } + }, + "system_health": { + "info": { + "can_reach_server": "Keterjangkauan server" + } } } \ No newline at end of file diff --git a/homeassistant/components/nfandroidtv/translations/ja.json b/homeassistant/components/nfandroidtv/translations/ja.json index 3db6768efcb..e096c6219f4 100644 --- a/homeassistant/components/nfandroidtv/translations/ja.json +++ b/homeassistant/components/nfandroidtv/translations/ja.json @@ -13,7 +13,7 @@ "host": "\u30db\u30b9\u30c8", "name": "\u540d\u524d" }, - "description": "\u3053\u306e\u7d71\u5408\u306b\u306f\u3001AndroidTV\u30a2\u30d7\u30ea\u306e\u901a\u77e5\u304c\u5fc5\u8981\u3067\u3059\u3002 \n\nAndroid TV\u306e\u5834\u5408: https://play.google.com/store/apps/details?id=de.cyberdream.androidtv.notifications.google\nFire TV\u306e\u5834\u5408: https://www.amazon.com/Christian-Fees-Notifications-for-Fire/dp/B00OESCXEK\n\n\u30eb\u30fc\u30bf\u30fc\u306eDHCP\u4e88\u7d04((DHCP reservation)\u30eb\u30fc\u30bf\u30fc\u306e\u30e6\u30fc\u30b6\u30fc\u30de\u30cb\u30e5\u30a2\u30eb\u3092\u53c2\u7167))\u307e\u305f\u306f\u3001\u30c7\u30d0\u30a4\u30b9\u306b\u9759\u7684IP\u30a2\u30c9\u30ec\u30b9\u3092\u8a2d\u5b9a\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002\u305d\u3046\u3067\u306a\u3044\u5834\u5408\u3001\u30c7\u30d0\u30a4\u30b9\u306f\u6700\u7d42\u7684\u306b\u4f7f\u7528\u3067\u304d\u306a\u304f\u306a\u308a\u307e\u3059\u3002" + "description": "\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u3092\u53c2\u7167\u3057\u3066\u3001\u3059\u3079\u3066\u306e\u8981\u4ef6\u304c\u6e80\u305f\u3055\u308c\u3066\u3044\u308b\u3053\u3068\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002" } } } diff --git a/homeassistant/components/nibe_heatpump/__init__.py b/homeassistant/components/nibe_heatpump/__init__.py new file mode 100644 index 00000000000..b9921df4e1e --- /dev/null +++ b/homeassistant/components/nibe_heatpump/__init__.py @@ -0,0 +1,293 @@ +"""The Nibe Heat Pump integration.""" +from __future__ import annotations + +from collections import defaultdict +from collections.abc import Callable, Iterable +from datetime import timedelta +from functools import cached_property +from typing import Any, Generic, TypeVar + +from nibe.coil import Coil +from nibe.connection import Connection +from nibe.connection.nibegw import NibeGW +from nibe.exceptions import CoilNotFoundException, CoilReadException +from nibe.heatpump import HeatPump, Model +from tenacity import RetryError, retry, retry_if_exception_type, stop_after_attempt + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONF_IP_ADDRESS, + CONF_MODEL, + EVENT_HOMEASSISTANT_STOP, + Platform, +) +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.entity import DeviceInfo, async_generate_entity_id +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, + UpdateFailed, +) + +from .const import ( + CONF_CONNECTION_TYPE, + CONF_CONNECTION_TYPE_NIBEGW, + CONF_LISTENING_PORT, + CONF_REMOTE_READ_PORT, + CONF_REMOTE_WRITE_PORT, + CONF_WORD_SWAP, + DOMAIN, + LOGGER, +) + +PLATFORMS: list[Platform] = [ + Platform.BINARY_SENSOR, + Platform.NUMBER, + Platform.SELECT, + Platform.SENSOR, + Platform.SWITCH, +] +COIL_READ_RETRIES = 5 + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Nibe Heat Pump from a config entry.""" + + heatpump = HeatPump(Model[entry.data[CONF_MODEL]]) + heatpump.word_swap = entry.data[CONF_WORD_SWAP] + await hass.async_add_executor_job(heatpump.initialize) + + connection_type = entry.data[CONF_CONNECTION_TYPE] + + if connection_type == CONF_CONNECTION_TYPE_NIBEGW: + connection = NibeGW( + heatpump, + entry.data[CONF_IP_ADDRESS], + entry.data[CONF_REMOTE_READ_PORT], + entry.data[CONF_REMOTE_WRITE_PORT], + listening_port=entry.data[CONF_LISTENING_PORT], + ) + else: + raise HomeAssistantError(f"Connection type {connection_type} is not supported.") + + await connection.start() + + entry.async_on_unload( + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, connection.stop) + ) + + coordinator = Coordinator(hass, heatpump, connection) + + data = hass.data.setdefault(DOMAIN, {}) + data[entry.entry_id] = coordinator + + reg = dr.async_get(hass) + reg.async_get_or_create( + config_entry_id=entry.entry_id, + identifiers={(DOMAIN, entry.unique_id or entry.entry_id)}, + manufacturer="NIBE Energy Systems", + model=heatpump.model.name, + name=heatpump.model.name, + ) + + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + + # Trigger a refresh again now that all platforms have registered + hass.async_create_task(coordinator.async_refresh()) + 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: Coordinator = hass.data[DOMAIN].pop(entry.entry_id) + await coordinator.connection.stop() + + return unload_ok + + +_DataTypeT = TypeVar("_DataTypeT") +_ContextTypeT = TypeVar("_ContextTypeT") + + +class ContextCoordinator( + Generic[_DataTypeT, _ContextTypeT], DataUpdateCoordinator[_DataTypeT] +): + """Update coordinator with context adjustments.""" + + @cached_property + def context_callbacks(self) -> dict[_ContextTypeT, list[CALLBACK_TYPE]]: + """Return a dict of all callbacks registered for a given context.""" + callbacks: dict[_ContextTypeT, list[CALLBACK_TYPE]] = defaultdict(list) + for update_callback, context in list(self._listeners.values()): + assert isinstance(context, set) + for address in context: + callbacks[address].append(update_callback) + return callbacks + + @callback + def async_update_context_listeners(self, contexts: Iterable[_ContextTypeT]) -> None: + """Update all listeners given a set of contexts.""" + update_callbacks: set[CALLBACK_TYPE] = set() + for context in contexts: + update_callbacks.update(self.context_callbacks.get(context, [])) + + for update_callback in update_callbacks: + update_callback() + + @callback + def async_add_listener( + self, update_callback: CALLBACK_TYPE, context: Any = None + ) -> Callable[[], None]: + """Wrap standard function to prune cached callback database.""" + release = super().async_add_listener(update_callback, context) + self.__dict__.pop("context_callbacks", None) + + @callback + def release_update(): + release() + self.__dict__.pop("context_callbacks", None) + + return release_update + + +class Coordinator(ContextCoordinator[dict[int, Coil], int]): + """Update coordinator for nibe heat pumps.""" + + config_entry: ConfigEntry + + def __init__( + self, + hass: HomeAssistant, + heatpump: HeatPump, + connection: Connection, + ) -> None: + """Initialize coordinator.""" + super().__init__( + hass, LOGGER, name="Nibe Heat Pump", update_interval=timedelta(seconds=60) + ) + + self.data = {} + self.seed: dict[int, Coil] = {} + self.connection = connection + self.heatpump = heatpump + + heatpump.subscribe(heatpump.COIL_UPDATE_EVENT, self._on_coil_update) + + def _on_coil_update(self, coil: Coil): + """Handle callback on coil updates.""" + self.data[coil.address] = coil + self.seed[coil.address] = coil + self.async_update_context_listeners([coil.address]) + + @property + def coils(self) -> list[Coil]: + """Return the full coil database.""" + return self.heatpump.get_coils() + + @property + def unique_id(self) -> str: + """Return unique id for this coordinator.""" + return self.config_entry.unique_id or self.config_entry.entry_id + + @property + def device_info(self) -> DeviceInfo: + """Return device information for the main device.""" + return DeviceInfo(identifiers={(DOMAIN, self.unique_id)}) + + def get_coil_value(self, coil: Coil) -> int | str | float | None: + """Return a coil with data and check for validity.""" + if coil := self.data.get(coil.address): + return coil.value + return None + + def get_coil_float(self, coil: Coil) -> float | None: + """Return a coil with float and check for validity.""" + if value := self.get_coil_value(coil): + return float(value) + return None + + async def async_write_coil(self, coil: Coil, value: int | float | str) -> None: + """Write coil and update state.""" + coil.value = value + coil = await self.connection.write_coil(coil) + + self.data[coil.address] = coil + + self.async_update_context_listeners([coil.address]) + + async def _async_update_data(self) -> dict[int, Coil]: + @retry( + retry=retry_if_exception_type(CoilReadException), + stop=stop_after_attempt(COIL_READ_RETRIES), + ) + async def read_coil(coil: Coil): + return await self.connection.read_coil(coil) + + result: dict[int, Coil] = {} + + for address in self.context_callbacks.keys(): + if seed := self.seed.pop(address, None): + self.logger.debug("Skipping seeded coil: %d", address) + result[address] = seed + continue + + try: + coil = self.heatpump.get_coil_by_address(address) + except CoilNotFoundException as exception: + self.logger.debug("Skipping missing coil: %s", exception) + continue + + try: + result[coil.address] = await read_coil(coil) + except (CoilReadException, RetryError) as exception: + raise UpdateFailed(f"Failed to update: {exception}") from exception + + self.seed.pop(coil.address, None) + + return result + + +class CoilEntity(CoordinatorEntity[Coordinator]): + """Base for coil based entities.""" + + _attr_has_entity_name = True + _attr_entity_registry_enabled_default = False + + def __init__( + self, coordinator: Coordinator, coil: Coil, entity_format: str + ) -> None: + """Initialize base entity.""" + super().__init__(coordinator, {coil.address}) + self.entity_id = async_generate_entity_id( + entity_format, coil.name, hass=coordinator.hass + ) + self._attr_name = coil.title + self._attr_unique_id = f"{coordinator.unique_id}-{coil.address}" + self._attr_device_info = coordinator.device_info + self._coil = coil + + @property + def available(self) -> bool: + """Return if entity is available.""" + return self.coordinator.last_update_success and self._coil.address in ( + self.coordinator.data or {} + ) + + def _async_read_coil(self, coil: Coil): + """Update state of entity based on coil data.""" + + async def _async_write_coil(self, value: int | float | str): + """Write coil and update state.""" + await self.coordinator.async_write_coil(self._coil, value) + + def _handle_coordinator_update(self) -> None: + coil = self.coordinator.data.get(self._coil.address) + if coil is None: + return + + self._coil = coil + self._async_read_coil(coil) + self.async_write_ha_state() diff --git a/homeassistant/components/nibe_heatpump/binary_sensor.py b/homeassistant/components/nibe_heatpump/binary_sensor.py new file mode 100644 index 00000000000..dda530f8457 --- /dev/null +++ b/homeassistant/components/nibe_heatpump/binary_sensor.py @@ -0,0 +1,41 @@ +"""The Nibe Heat Pump binary sensors.""" +from __future__ import annotations + +from nibe.coil import Coil + +from homeassistant.components.binary_sensor import ENTITY_ID_FORMAT, BinarySensorEntity +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 DOMAIN, CoilEntity, Coordinator + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up platform.""" + + coordinator: Coordinator = hass.data[DOMAIN][config_entry.entry_id] + + async_add_entities( + BinarySensor(coordinator, coil) + for coil in coordinator.coils + if not coil.is_writable and coil.is_boolean + ) + + +class BinarySensor(CoilEntity, BinarySensorEntity): + """Binary sensor entity.""" + + _attr_entity_category = EntityCategory.DIAGNOSTIC + + def __init__(self, coordinator: Coordinator, coil: Coil) -> None: + """Initialize entity.""" + super().__init__(coordinator, coil, ENTITY_ID_FORMAT) + + def _async_read_coil(self, coil: Coil) -> None: + self._attr_is_on = coil.value == "ON" diff --git a/homeassistant/components/nibe_heatpump/config_flow.py b/homeassistant/components/nibe_heatpump/config_flow.py new file mode 100644 index 00000000000..28fafdb3a37 --- /dev/null +++ b/homeassistant/components/nibe_heatpump/config_flow.py @@ -0,0 +1,133 @@ +"""Config flow for Nibe Heat Pump integration.""" +from __future__ import annotations + +import errno +from typing import Any + +from nibe.connection.nibegw import NibeGW +from nibe.exceptions import CoilNotFoundException, CoilReadException, CoilWriteException +from nibe.heatpump import HeatPump, Model +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_IP_ADDRESS, CONF_MODEL +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers import config_validation as cv +from homeassistant.util.network import is_ipv4_address + +from .const import ( + CONF_CONNECTION_TYPE, + CONF_CONNECTION_TYPE_NIBEGW, + CONF_LISTENING_PORT, + CONF_REMOTE_READ_PORT, + CONF_REMOTE_WRITE_PORT, + CONF_WORD_SWAP, + DOMAIN, + LOGGER, +) + +STEP_USER_DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_MODEL): vol.In([e.name for e in Model]), + vol.Required(CONF_IP_ADDRESS): str, + vol.Required(CONF_LISTENING_PORT): cv.port, + vol.Required(CONF_REMOTE_READ_PORT): cv.port, + vol.Required(CONF_REMOTE_WRITE_PORT): cv.port, + } +) + + +class FieldError(Exception): + """Field with invalid data.""" + + def __init__(self, message: str, field: str, error: str) -> None: + """Set up error.""" + super().__init__(message) + self.field = field + self.error = error + + +async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]: + """Validate the user input allows us to connect.""" + + if not is_ipv4_address(data[CONF_IP_ADDRESS]): + raise FieldError("Not a valid ipv4 address", CONF_IP_ADDRESS, "address") + + heatpump = HeatPump(Model[data[CONF_MODEL]]) + heatpump.initialize() + + connection = NibeGW( + heatpump, + data[CONF_IP_ADDRESS], + data[CONF_REMOTE_READ_PORT], + data[CONF_REMOTE_WRITE_PORT], + listening_port=data[CONF_LISTENING_PORT], + ) + + try: + await connection.start() + except OSError as exception: + if exception.errno == errno.EADDRINUSE: + raise FieldError( + "Address already in use", "listening_port", "address_in_use" + ) from exception + raise + + try: + coil = heatpump.get_coil_by_name("modbus40-word-swap-48852") + coil = await connection.read_coil(coil) + word_swap = coil.value == "ON" + coil = await connection.write_coil(coil) + except CoilNotFoundException as exception: + raise FieldError( + "Model selected doesn't seem to support expected coils", "base", "model" + ) from exception + except CoilReadException as exception: + raise FieldError("Timeout on read from pump", "base", "read") from exception + except CoilWriteException as exception: + raise FieldError("Timeout on writing to pump", "base", "write") from exception + finally: + await connection.stop() + + return { + "title": f"{data[CONF_MODEL]} at {data[CONF_IP_ADDRESS]}", + CONF_WORD_SWAP: word_swap, + } + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Nibe Heat Pump.""" + + VERSION = 1 + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the initial step.""" + if user_input is None: + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA + ) + + errors = {} + + try: + info = await validate_input(self.hass, user_input) + except FieldError as exception: + LOGGER.debug("Validation error %s", exception) + errors[exception.field] = exception.error + except Exception: # pylint: disable=broad-except + LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + data = { + **user_input, + CONF_WORD_SWAP: info[CONF_WORD_SWAP], + CONF_CONNECTION_TYPE: CONF_CONNECTION_TYPE_NIBEGW, + } + return self.async_create_entry(title=info["title"], data=data) + + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors + ) diff --git a/homeassistant/components/nibe_heatpump/const.py b/homeassistant/components/nibe_heatpump/const.py new file mode 100644 index 00000000000..f1bcbf11127 --- /dev/null +++ b/homeassistant/components/nibe_heatpump/const.py @@ -0,0 +1,12 @@ +"""Constants for the Nibe Heat Pump integration.""" +import logging + +DOMAIN = "nibe_heatpump" +LOGGER = logging.getLogger(__package__) + +CONF_LISTENING_PORT = "listening_port" +CONF_REMOTE_READ_PORT = "remote_read_port" +CONF_REMOTE_WRITE_PORT = "remote_write_port" +CONF_WORD_SWAP = "word_swap" +CONF_CONNECTION_TYPE = "connection_type" +CONF_CONNECTION_TYPE_NIBEGW = "nibegw" diff --git a/homeassistant/components/nibe_heatpump/manifest.json b/homeassistant/components/nibe_heatpump/manifest.json new file mode 100644 index 00000000000..4b66b93d31b --- /dev/null +++ b/homeassistant/components/nibe_heatpump/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "nibe_heatpump", + "name": "Nibe Heat Pump", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/nibe_heatpump", + "requirements": ["nibe==0.5.0", "tenacity==8.0.1"], + "codeowners": ["@elupus"], + "iot_class": "local_polling" +} diff --git a/homeassistant/components/nibe_heatpump/number.py b/homeassistant/components/nibe_heatpump/number.py new file mode 100644 index 00000000000..11c6917ec1c --- /dev/null +++ b/homeassistant/components/nibe_heatpump/number.py @@ -0,0 +1,68 @@ +"""The Nibe Heat Pump numbers.""" +from __future__ import annotations + +from nibe.coil import Coil + +from homeassistant.components.number import ENTITY_ID_FORMAT, 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 DOMAIN, CoilEntity, Coordinator + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up platform.""" + + coordinator: Coordinator = hass.data[DOMAIN][config_entry.entry_id] + + async_add_entities( + Number(coordinator, coil) + for coil in coordinator.coils + if coil.is_writable and not coil.mappings + ) + + +def _get_numeric_limits(size: str): + """Calculate the integer limits of a signed or unsigned integer value.""" + if size[0] == "u": + return (0, pow(2, int(size[1:])) - 1) + if size[0] == "s": + return (-pow(2, int(size[1:]) - 1), pow(2, int(size[1:]) - 1) - 1) + raise ValueError(f"Invalid size type specified {size}") + + +class Number(CoilEntity, NumberEntity): + """Number entity.""" + + _attr_entity_category = EntityCategory.CONFIG + + def __init__(self, coordinator: Coordinator, coil: Coil) -> None: + """Initialize entity.""" + super().__init__(coordinator, coil, ENTITY_ID_FORMAT) + if coil.min is None or coil.max is None: + ( + self._attr_native_min_value, + self._attr_native_max_value, + ) = _get_numeric_limits(coil.size) + else: + self._attr_native_min_value = float(coil.min) + self._attr_native_max_value = float(coil.max) + + self._attr_native_unit_of_measurement = coil.unit + self._attr_native_value = None + + def _async_read_coil(self, coil: Coil) -> None: + try: + self._attr_native_value = float(coil.value) + except ValueError: + self._attr_native_value = None + + async def async_set_native_value(self, value: float) -> None: + """Set new value.""" + await self._async_write_coil(value) diff --git a/homeassistant/components/nibe_heatpump/select.py b/homeassistant/components/nibe_heatpump/select.py new file mode 100644 index 00000000000..27df1980287 --- /dev/null +++ b/homeassistant/components/nibe_heatpump/select.py @@ -0,0 +1,47 @@ +"""The Nibe Heat Pump select.""" +from __future__ import annotations + +from nibe.coil import Coil + +from homeassistant.components.select import ENTITY_ID_FORMAT, SelectEntity +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 DOMAIN, CoilEntity, Coordinator + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up platform.""" + + coordinator: Coordinator = hass.data[DOMAIN][config_entry.entry_id] + + async_add_entities( + Select(coordinator, coil) + for coil in coordinator.coils + if coil.is_writable and coil.mappings and not coil.is_boolean + ) + + +class Select(CoilEntity, SelectEntity): + """Select entity.""" + + _attr_entity_category = EntityCategory.CONFIG + + def __init__(self, coordinator: Coordinator, coil: Coil) -> None: + """Initialize entity.""" + super().__init__(coordinator, coil, ENTITY_ID_FORMAT) + self._attr_options = list(coil.mappings.values()) + self._attr_current_option = None + + def _async_read_coil(self, coil: Coil) -> None: + self._attr_current_option = coil.value + + async def async_select_option(self, option: str) -> None: + """Support writing value.""" + await self._async_write_coil(option) diff --git a/homeassistant/components/nibe_heatpump/sensor.py b/homeassistant/components/nibe_heatpump/sensor.py new file mode 100644 index 00000000000..b0bc816dad6 --- /dev/null +++ b/homeassistant/components/nibe_heatpump/sensor.py @@ -0,0 +1,138 @@ +"""The Nibe Heat Pump sensors.""" +from __future__ import annotations + +from nibe.coil import Coil + +from homeassistant.components.sensor import ( + ENTITY_ID_FORMAT, + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + ELECTRIC_CURRENT_AMPERE, + ELECTRIC_CURRENT_MILLIAMPERE, + ELECTRIC_POTENTIAL_MILLIVOLT, + ELECTRIC_POTENTIAL_VOLT, + ENERGY_KILO_WATT_HOUR, + ENERGY_MEGA_WATT_HOUR, + ENERGY_WATT_HOUR, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, + TIME_HOURS, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import DOMAIN, CoilEntity, Coordinator + +UNIT_DESCRIPTIONS = { + "°C": SensorEntityDescription( + key="°C", + entity_category=EntityCategory.DIAGNOSTIC, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=TEMP_CELSIUS, + ), + "°F": SensorEntityDescription( + key="°F", + entity_category=EntityCategory.DIAGNOSTIC, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=TEMP_FAHRENHEIT, + ), + "A": SensorEntityDescription( + key="A", + entity_category=EntityCategory.DIAGNOSTIC, + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + ), + "mA": SensorEntityDescription( + key="mA", + entity_category=EntityCategory.DIAGNOSTIC, + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=ELECTRIC_CURRENT_MILLIAMPERE, + ), + "V": SensorEntityDescription( + key="V", + entity_category=EntityCategory.DIAGNOSTIC, + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + ), + "mV": SensorEntityDescription( + key="mV", + entity_category=EntityCategory.DIAGNOSTIC, + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=ELECTRIC_POTENTIAL_MILLIVOLT, + ), + "Wh": SensorEntityDescription( + key="Wh", + entity_category=EntityCategory.DIAGNOSTIC, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.TOTAL_INCREASING, + native_unit_of_measurement=ENERGY_WATT_HOUR, + ), + "kWh": SensorEntityDescription( + key="kWh", + entity_category=EntityCategory.DIAGNOSTIC, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.TOTAL_INCREASING, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + ), + "MWh": SensorEntityDescription( + key="MWh", + entity_category=EntityCategory.DIAGNOSTIC, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.TOTAL_INCREASING, + native_unit_of_measurement=ENERGY_MEGA_WATT_HOUR, + ), + "h": SensorEntityDescription( + key="h", + entity_category=EntityCategory.DIAGNOSTIC, + device_class=SensorDeviceClass.DURATION, + native_unit_of_measurement=TIME_HOURS, + ), +} + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up platform.""" + + coordinator: Coordinator = hass.data[DOMAIN][config_entry.entry_id] + + async_add_entities( + Sensor(coordinator, coil, UNIT_DESCRIPTIONS.get(coil.unit)) + for coil in coordinator.coils + if not coil.is_writable and not coil.is_boolean + ) + + +class Sensor(CoilEntity, SensorEntity): + """Sensor entity.""" + + def __init__( + self, + coordinator: Coordinator, + coil: Coil, + entity_description: SensorEntityDescription | None, + ) -> None: + """Initialize entity.""" + super().__init__(coordinator, coil, ENTITY_ID_FORMAT) + if entity_description: + self.entity_description = entity_description + else: + self._attr_native_unit_of_measurement = coil.unit + + def _async_read_coil(self, coil: Coil): + self._attr_native_value = coil.value diff --git a/homeassistant/components/nibe_heatpump/strings.json b/homeassistant/components/nibe_heatpump/strings.json new file mode 100644 index 00000000000..45e55b61083 --- /dev/null +++ b/homeassistant/components/nibe_heatpump/strings.json @@ -0,0 +1,22 @@ +{ + "config": { + "step": { + "user": { + "data": { + "ip_address": "Remote IP address", + "remote_read_port": "Remote read port", + "remote_write_port": "Remote write port", + "listening_port": "Local listening port" + } + } + }, + "error": { + "write": "Error on write request to pump. Verify your `Remote write port` or `Remote IP address`.", + "read": "Error on read request from pump. Verify your `Remote read port` or `Remote IP address`.", + "address": "Invalid remote IP address specified. Address must be a IPV4 address.", + "address_in_use": "The selected listening port is already in use on this system.", + "model": "The model selected doesn't seem to support modbus40", + "unknown": "[%key:common::config_flow::error::unknown%]" + } + } +} diff --git a/homeassistant/components/nibe_heatpump/switch.py b/homeassistant/components/nibe_heatpump/switch.py new file mode 100644 index 00000000000..133e3d7244c --- /dev/null +++ b/homeassistant/components/nibe_heatpump/switch.py @@ -0,0 +1,52 @@ +"""The Nibe Heat Pump switch.""" +from __future__ import annotations + +from typing import Any + +from nibe.coil import Coil + +from homeassistant.components.switch import ENTITY_ID_FORMAT, SwitchEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import DOMAIN, CoilEntity, Coordinator + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up platform.""" + + coordinator: Coordinator = hass.data[DOMAIN][config_entry.entry_id] + + async_add_entities( + Switch(coordinator, coil) + for coil in coordinator.coils + if coil.is_writable and coil.is_boolean + ) + + +class Switch(CoilEntity, SwitchEntity): + """Switch entity.""" + + _attr_entity_category = EntityCategory.CONFIG + + def __init__(self, coordinator: Coordinator, coil: Coil) -> None: + """Initialize entity.""" + super().__init__(coordinator, coil, ENTITY_ID_FORMAT) + self._attr_is_on = None + + def _async_read_coil(self, coil: Coil) -> None: + self._attr_is_on = coil.value == "ON" + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the entity on.""" + await self._async_write_coil("ON") + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the entity off.""" + await self._async_write_coil("OFF") diff --git a/homeassistant/components/nibe_heatpump/translations/bg.json b/homeassistant/components/nibe_heatpump/translations/bg.json new file mode 100644 index 00000000000..88f52d84269 --- /dev/null +++ b/homeassistant/components/nibe_heatpump/translations/bg.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, + "error": { + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nibe_heatpump/translations/ca.json b/homeassistant/components/nibe_heatpump/translations/ca.json new file mode 100644 index 00000000000..95cc0f32841 --- /dev/null +++ b/homeassistant/components/nibe_heatpump/translations/ca.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "error": { + "address": "Adre\u00e7a IP remota inv\u00e0lida. L'adre\u00e7a ha de ser una adre\u00e7a IPV4.", + "address_in_use": "El port d'escolta seleccionat ja est\u00e0 en \u00fas en aquest sistema.", + "model": "El model seleccionat no sembla admetre modbus40", + "read": "Error en la sol\u00b7licitud de lectura de la bomba. Verifica el port remot de lectura i/o l'adre\u00e7a IP remota.", + "unknown": "Error inesperat", + "write": "Error en la sol\u00b7licitud d'escriptura a la bomba. Verifica el port remot d'escriptura i/o l'adre\u00e7a IP remota." + }, + "step": { + "user": { + "data": { + "ip_address": "Adre\u00e7a IP remota", + "listening_port": "Port local d'escolta", + "remote_read_port": "Port remot de lectura", + "remote_write_port": "Port remot d'escriptura" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nibe_heatpump/translations/de.json b/homeassistant/components/nibe_heatpump/translations/de.json new file mode 100644 index 00000000000..8eda1b68b8b --- /dev/null +++ b/homeassistant/components/nibe_heatpump/translations/de.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "address": "Ung\u00fcltige Remote-IP-Adresse angegeben. Die Adresse muss eine IPV4-Adresse sein.", + "address_in_use": "Der ausgew\u00e4hlte Listening-Port wird auf diesem System bereits verwendet.", + "model": "Das ausgew\u00e4hlte Modell scheint modbus40 nicht zu unterst\u00fctzen", + "read": "Fehler bei Leseanforderung von Pumpe. \u00dcberpr\u00fcfe deinen \u201eRemote-Leseport\u201c oder \u201eRemote-IP-Adresse\u201c.", + "unknown": "Unerwarteter Fehler", + "write": "Fehler bei Schreibanforderung an Pumpe. \u00dcberpr\u00fcfe deinen \u201eRemote-Schreibport\u201c oder \u201eRemote-IP-Adresse\u201c." + }, + "step": { + "user": { + "data": { + "ip_address": "Remote-IP-Adresse", + "listening_port": "Lokaler Leseport", + "remote_read_port": "Remote-Leseport", + "remote_write_port": "Remote-Schreibport" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nibe_heatpump/translations/el.json b/homeassistant/components/nibe_heatpump/translations/el.json new file mode 100644 index 00000000000..a740bc43742 --- /dev/null +++ b/homeassistant/components/nibe_heatpump/translations/el.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af" + }, + "error": { + "address": "\u039a\u03b1\u03b8\u03bf\u03c1\u03af\u03c3\u03c4\u03b7\u03ba\u03b5 \u03bc\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7 \u03b1\u03c0\u03bf\u03bc\u03b1\u03ba\u03c1\u03c5\u03c3\u03bc\u03ad\u03bd\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP. \u0397 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IPV4.", + "address_in_use": "\u0397 \u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03b7 \u03b8\u03cd\u03c1\u03b1 \u03b1\u03ba\u03c1\u03cc\u03b1\u03c3\u03b7\u03c2 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03ae\u03b4\u03b7 \u03c3\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c3\u03cd\u03c3\u03c4\u03b7\u03bc\u03b1.", + "model": "\u03a4\u03bf \u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03bf \u03bc\u03bf\u03bd\u03c4\u03ad\u03bb\u03bf \u03b4\u03b5\u03bd \u03c6\u03b1\u03af\u03bd\u03b5\u03c4\u03b1\u03b9 \u03bd\u03b1 \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03b9 modbus40", + "read": "\u03a3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03c3\u03c4\u03bf \u03b1\u03af\u03c4\u03b7\u03bc\u03b1 \u03b1\u03bd\u03ac\u03b3\u03bd\u03c9\u03c3\u03b7\u03c2 \u03b1\u03c0\u03cc \u03c4\u03b7\u03bd \u03b1\u03bd\u03c4\u03bb\u03af\u03b1. \u0395\u03c0\u03b1\u03bb\u03b7\u03b8\u03b5\u03cd\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \"\u0391\u03c0\u03bf\u03bc\u03b1\u03ba\u03c1\u03c5\u03c3\u03bc\u03ad\u03bd\u03b7 \u03b8\u03cd\u03c1\u03b1 \u03b5\u03b3\u03b3\u03c1\u03b1\u03c6\u03ae\u03c2\" \u03ae \u03c4\u03b7\u03bd \"\u0391\u03c0\u03bf\u03bc\u03b1\u03ba\u03c1\u03c5\u03c3\u03bc\u03ad\u03bd\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP\".", + "unknown": "\u0391\u03c0\u03c1\u03bf\u03c3\u03b4\u03cc\u03ba\u03b7\u03c4\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1", + "write": "\u03a3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03c3\u03c4\u03bf \u03b1\u03af\u03c4\u03b7\u03bc\u03b1 \u03b5\u03b3\u03b3\u03c1\u03b1\u03c6\u03ae\u03c2 \u03c3\u03c4\u03b7\u03bd \u03b1\u03bd\u03c4\u03bb\u03af\u03b1. \u0395\u03c0\u03b1\u03bb\u03b7\u03b8\u03b5\u03cd\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \"\u0391\u03c0\u03bf\u03bc\u03b1\u03ba\u03c1\u03c5\u03c3\u03bc\u03ad\u03bd\u03b7 \u03b8\u03cd\u03c1\u03b1 \u03b5\u03b3\u03b3\u03c1\u03b1\u03c6\u03ae\u03c2\" \u03ae \u03c4\u03b7\u03bd \"\u0391\u03c0\u03bf\u03bc\u03b1\u03ba\u03c1\u03c5\u03c3\u03bc\u03ad\u03bd\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP\"." + }, + "step": { + "user": { + "data": { + "ip_address": "\u0391\u03c0\u03bf\u03bc\u03b1\u03ba\u03c1\u03c5\u03c3\u03bc\u03ad\u03bd\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP", + "listening_port": "\u03a4\u03bf\u03c0\u03b9\u03ba\u03ae \u03b8\u03cd\u03c1\u03b1 \u03b1\u03ba\u03c1\u03cc\u03b1\u03c3\u03b7\u03c2", + "remote_read_port": "\u0391\u03c0\u03bf\u03bc\u03b1\u03ba\u03c1\u03c5\u03c3\u03bc\u03ad\u03bd\u03b7 \u03b8\u03cd\u03c1\u03b1 \u03b1\u03bd\u03ac\u03b3\u03bd\u03c9\u03c3\u03b7\u03c2", + "remote_write_port": "\u0391\u03c0\u03bf\u03bc\u03b1\u03ba\u03c1\u03c5\u03c3\u03bc\u03ad\u03bd\u03b7 \u03b8\u03cd\u03c1\u03b1 \u03b5\u03b3\u03b3\u03c1\u03b1\u03c6\u03ae\u03c2" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nibe_heatpump/translations/en.json b/homeassistant/components/nibe_heatpump/translations/en.json new file mode 100644 index 00000000000..74dd8313e95 --- /dev/null +++ b/homeassistant/components/nibe_heatpump/translations/en.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "address": "Invalid remote IP address specified. Address must be a IPV4 address.", + "address_in_use": "The selected listening port is already in use on this system.", + "model": "The model selected doesn't seem to support modbus40", + "read": "Error on read request from pump. Verify your `Remote read port` or `Remote IP address`.", + "unknown": "Unexpected error", + "write": "Error on write request to pump. Verify your `Remote write port` or `Remote IP address`." + }, + "step": { + "user": { + "data": { + "ip_address": "Remote IP address", + "listening_port": "Local listening port", + "remote_read_port": "Remote read port", + "remote_write_port": "Remote write port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nibe_heatpump/translations/es.json b/homeassistant/components/nibe_heatpump/translations/es.json new file mode 100644 index 00000000000..60c228a28e7 --- /dev/null +++ b/homeassistant/components/nibe_heatpump/translations/es.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "address": "Se especific\u00f3 una direcci\u00f3n IP remota no v\u00e1lida. La direcci\u00f3n debe ser una direcci\u00f3n IPv4.", + "address_in_use": "El puerto de escucha seleccionado ya est\u00e1 en uso en este sistema.", + "model": "El modelo seleccionado no parece ser compatible con modbus40", + "read": "Error en la solicitud de lectura de la bomba. Verifica tu `Puerto de lectura remoto` o `Direcci\u00f3n IP remota`.", + "unknown": "Error inesperado", + "write": "Error en la solicitud de escritura a la bomba. Verifica tu `Puerto de escritura remoto` o `Direcci\u00f3n IP remota`." + }, + "step": { + "user": { + "data": { + "ip_address": "Direcci\u00f3n IP remota", + "listening_port": "Puerto de escucha local", + "remote_read_port": "Puerto de lectura remoto", + "remote_write_port": "Puerto de escritura remoto" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nibe_heatpump/translations/fr.json b/homeassistant/components/nibe_heatpump/translations/fr.json new file mode 100644 index 00000000000..dc28a729aea --- /dev/null +++ b/homeassistant/components/nibe_heatpump/translations/fr.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "unknown": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "ip_address": "Adresse IP distante", + "listening_port": "Port d'\u00e9coute local", + "remote_read_port": "Port de lecture distant", + "remote_write_port": "Port d'\u00e9criture distant" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nibe_heatpump/translations/he.json b/homeassistant/components/nibe_heatpump/translations/he.json new file mode 100644 index 00000000000..ea40181bd9a --- /dev/null +++ b/homeassistant/components/nibe_heatpump/translations/he.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nibe_heatpump/translations/hu.json b/homeassistant/components/nibe_heatpump/translations/hu.json new file mode 100644 index 00000000000..4fcff29a560 --- /dev/null +++ b/homeassistant/components/nibe_heatpump/translations/hu.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "address": "\u00c9rv\u00e9nytelen t\u00e1voli IP-c\u00edm van megadva. A c\u00edmnek IPV4-c\u00edmnek kell lennie.", + "address_in_use": "A kiv\u00e1lasztott port m\u00e1r haszn\u00e1latban van ezen a rendszeren.", + "model": "\u00dagy t\u0171nik, hogy a kiv\u00e1lasztott modell nem t\u00e1mogatja a modbus40-et", + "read": "Hiba a szivatty\u00fa olvas\u00e1si k\u00e9r\u00e9s\u00e9n\u00e9l. Ellen\u0151rizze a \"T\u00e1voli olvas\u00e1si portot\" vagy a \"T\u00e1voli IP-c\u00edmet\".", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt", + "write": "Hiba a h\u0151szivatty\u00fa \u00edr\u00e1si k\u00e9relm\u00e9ben. Ellen\u0151rizze a portot, c\u00edmet." + }, + "step": { + "user": { + "data": { + "ip_address": "T\u00e1voli IP-c\u00edm", + "listening_port": "Helyi port", + "remote_read_port": "T\u00e1voli olvas\u00e1si port", + "remote_write_port": "T\u00e1voli \u00edr\u00e1si port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nibe_heatpump/translations/id.json b/homeassistant/components/nibe_heatpump/translations/id.json new file mode 100644 index 00000000000..53e3d202877 --- /dev/null +++ b/homeassistant/components/nibe_heatpump/translations/id.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "address": "Alamat IP jarak jauh yang ditentukan tidak valid. Alamat harus berupa alamat IPv4.", + "address_in_use": "Port mendengarkan yang dipilih sudah digunakan pada sistem ini.", + "model": "Model yang dipilih tampaknya tidak mendukung modbus40", + "read": "Kesalahan pada permintaan baca dari pompa. Verifikasi `Port baca jarak jauh` atau `Alamat IP jarak jauh` Anda.", + "unknown": "Kesalahan yang tidak diharapkan", + "write": "Kesalahan pada permintaan tulis ke pompa. Verifikasi `Port tulis jarak jauh` atau `Alamat IP jarak jauh` Anda." + }, + "step": { + "user": { + "data": { + "ip_address": "Alamat IP jarak jauh", + "listening_port": "Port mendengarkan lokal", + "remote_read_port": "Port baca jarak jauh", + "remote_write_port": "Port tulis jarak jauh" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nibe_heatpump/translations/it.json b/homeassistant/components/nibe_heatpump/translations/it.json new file mode 100644 index 00000000000..9de61113160 --- /dev/null +++ b/homeassistant/components/nibe_heatpump/translations/it.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "address": "Indirizzo IP remoto specificato non valido. L'indirizzo deve essere un indirizzo IPV4.", + "address_in_use": "La porta di ascolto selezionata \u00e8 gi\u00e0 in uso su questo sistema.", + "model": "Il modello selezionato non sembra supportare il modbus40", + "read": "Errore su richiesta di lettura dalla pompa. Verifica la tua \"Porta di lettura remota\" o \"Indirizzo IP remoto\".", + "unknown": "Errore imprevisto", + "write": "Errore nella richiesta di scrittura alla pompa. Verifica la tua \"Porta di scrittura remota\" o \"Indirizzo IP remoto\"." + }, + "step": { + "user": { + "data": { + "ip_address": "Indirizzo IP remoto", + "listening_port": "Porta di ascolto locale", + "remote_read_port": "Porta di lettura remota", + "remote_write_port": "Porta di scrittura remota" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nibe_heatpump/translations/nl.json b/homeassistant/components/nibe_heatpump/translations/nl.json new file mode 100644 index 00000000000..c227699ff21 --- /dev/null +++ b/homeassistant/components/nibe_heatpump/translations/nl.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd" + }, + "error": { + "unknown": "Onverwachte fout" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nibe_heatpump/translations/no.json b/homeassistant/components/nibe_heatpump/translations/no.json new file mode 100644 index 00000000000..a9c4c41993d --- /dev/null +++ b/homeassistant/components/nibe_heatpump/translations/no.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "address": "Ugyldig ekstern IP-adresse er angitt. Adressen m\u00e5 v\u00e6re en IPV4-adresse.", + "address_in_use": "Den valgte lytteporten er allerede i bruk p\u00e5 dette systemet.", + "model": "Den valgte modellen ser ikke ut til \u00e5 st\u00f8tte modbus40", + "read": "Feil ved leseforesp\u00f8rsel fra pumpe. Bekreft din \"Ekstern leseport\" eller \"Ekstern IP-adresse\".", + "unknown": "Uventet feil", + "write": "Feil ved skriveforesp\u00f8rsel til pumpen. Bekreft din \"Ekstern skriveport\" eller \"Ekstern IP-adresse\"." + }, + "step": { + "user": { + "data": { + "ip_address": "Ekstern IP-adresse", + "listening_port": "Lokal lytteport", + "remote_read_port": "Ekstern leseport", + "remote_write_port": "Ekstern skriveport" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nibe_heatpump/translations/pl.json b/homeassistant/components/nibe_heatpump/translations/pl.json new file mode 100644 index 00000000000..7a179ad7326 --- /dev/null +++ b/homeassistant/components/nibe_heatpump/translations/pl.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + }, + "error": { + "address": "Podano nieprawid\u0142owy zdalny adres IP. Adres musi by\u0107 adresem IPV4.", + "address_in_use": "Wybrany port nas\u0142uchiwania jest ju\u017c u\u017cywany w tym systemie.", + "model": "Wybrany model nie obs\u0142uguje modbus40", + "read": "B\u0142\u0105d przy \u017c\u0105daniu odczytu z pompy. Sprawd\u017a \u201eZdalny port odczytu\u201d lub \u201eZdalny adres IP\u201d.", + "unknown": "Nieoczekiwany b\u0142\u0105d", + "write": "B\u0142\u0105d przy \u017c\u0105daniu zapisu do pompy. Sprawd\u017a \u201eZdalny port zapisu\u201d lub \u201eZdalny adres IP\u201d." + }, + "step": { + "user": { + "data": { + "ip_address": "Zdalny adres IP", + "listening_port": "Lokalny port nas\u0142uchiwania", + "remote_read_port": "Zdalny port odczytu", + "remote_write_port": "Zdalny port zapisu" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nibe_heatpump/translations/pt-BR.json b/homeassistant/components/nibe_heatpump/translations/pt-BR.json new file mode 100644 index 00000000000..127f6c6010b --- /dev/null +++ b/homeassistant/components/nibe_heatpump/translations/pt-BR.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "address": "Endere\u00e7o IP remoto inv\u00e1lido especificado. O endere\u00e7o deve ser um endere\u00e7o IPV4.", + "address_in_use": "A porta de escuta selecionada j\u00e1 est\u00e1 em uso neste sistema.", + "model": "O modelo selecionado parece n\u00e3o suportar modbus40", + "read": "Erro na solicita\u00e7\u00e3o de leitura da bomba. Verifique sua `Porta de leitura remota` ou `Endere\u00e7o IP remoto`.", + "unknown": "Erro inesperado", + "write": "Erro na solicita\u00e7\u00e3o de grava\u00e7\u00e3o para bombear. Verifique sua `Porta de grava\u00e7\u00e3o remota` ou `Endere\u00e7o IP remoto`." + }, + "step": { + "user": { + "data": { + "ip_address": "Endere\u00e7o IP remoto", + "listening_port": "Porta de escuta local", + "remote_read_port": "Porta de leitura remota", + "remote_write_port": "Porta de grava\u00e7\u00e3o remota" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nibe_heatpump/translations/ru.json b/homeassistant/components/nibe_heatpump/translations/ru.json new file mode 100644 index 00000000000..e1192c7e08c --- /dev/null +++ b/homeassistant/components/nibe_heatpump/translations/ru.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "address": "\u0423\u043a\u0430\u0437\u0430\u043d \u043d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0443\u0434\u0430\u043b\u0435\u043d\u043d\u044b\u0439 IP-\u0430\u0434\u0440\u0435\u0441. \u0421\u043b\u0435\u0434\u0443\u0435\u0442 \u0443\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u0430\u0434\u0440\u0435\u0441 IPV4.", + "address_in_use": "\u0412\u044b\u0431\u0440\u0430\u043d\u043d\u044b\u0439 \u043f\u043e\u0440\u0442 \u043f\u0440\u043e\u0441\u043b\u0443\u0448\u0438\u0432\u0430\u043d\u0438\u044f \u0443\u0436\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u0432 \u044d\u0442\u043e\u0439 \u0441\u0438\u0441\u0442\u0435\u043c\u0435.", + "model": "\u0412\u044b\u0431\u0440\u0430\u043d\u043d\u0430\u044f \u043c\u043e\u0434\u0435\u043b\u044c \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 modbus40.", + "read": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u0437\u0430\u043f\u0440\u043e\u0441\u0435 \u043d\u0430 \u0447\u0442\u0435\u043d\u0438\u0435. \u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b `\u0443\u0434\u0430\u043b\u0435\u043d\u043d\u044b\u0439 \u043f\u043e\u0440\u0442 \u0447\u0442\u0435\u043d\u0438\u044f` \u0438\u043b\u0438 `\u0443\u0434\u0430\u043b\u0435\u043d\u043d\u044b\u0439 IP-\u0430\u0434\u0440\u0435\u0441`.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430.", + "write": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u0437\u0430\u043f\u0440\u043e\u0441\u0435 \u043d\u0430 \u0437\u0430\u043f\u0438\u0441\u044c. \u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b `\u0443\u0434\u0430\u043b\u0435\u043d\u043d\u044b\u0439 \u043f\u043e\u0440\u0442 \u0437\u0430\u043f\u0438\u0441\u0438` \u0438\u043b\u0438 `\u0443\u0434\u0430\u043b\u0435\u043d\u043d\u044b\u0439 IP-\u0430\u0434\u0440\u0435\u0441`." + }, + "step": { + "user": { + "data": { + "ip_address": "\u0423\u0434\u0430\u043b\u0435\u043d\u043d\u044b\u0439 IP-\u0430\u0434\u0440\u0435\u0441", + "listening_port": "\u041b\u043e\u043a\u0430\u043b\u044c\u043d\u044b\u0439 \u043f\u043e\u0440\u0442 \u043f\u0440\u043e\u0441\u043b\u0443\u0448\u0438\u0432\u0430\u043d\u0438\u044f", + "remote_read_port": "\u0423\u0434\u0430\u043b\u0435\u043d\u043d\u044b\u0439 \u043f\u043e\u0440\u0442 \u0447\u0442\u0435\u043d\u0438\u044f", + "remote_write_port": "\u0423\u0434\u0430\u043b\u0435\u043d\u043d\u044b\u0439 \u043f\u043e\u0440\u0442 \u0437\u0430\u043f\u0438\u0441\u0438" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nibe_heatpump/translations/zh-Hant.json b/homeassistant/components/nibe_heatpump/translations/zh-Hant.json new file mode 100644 index 00000000000..a2bb8c8f023 --- /dev/null +++ b/homeassistant/components/nibe_heatpump/translations/zh-Hant.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "address": "\u6307\u5b9a\u7684\u9060\u7aef IP \u4f4d\u5740\u7121\u6548\u3002\u4f4d\u5740\u5fc5\u9808\u70ba IPV4 \u4f4d\u5740\u3002", + "address_in_use": "\u6240\u9078\u64c7\u7684\u76e3\u807d\u901a\u8a0a\u57e0\u5df2\u7d93\u88ab\u7cfb\u7d71\u6240\u4f7f\u7528\u3002", + "model": "\u6240\u9078\u64c7\u7684\u578b\u865f\u4f3c\u4e4e\u4e0d\u652f\u63f4 modbus40", + "read": "\u8b80\u53d6\u8acb\u6c42\u767c\u751f\u932f\u8aa4\uff0c\u8acb\u78ba\u8a8d `\u9060\u7aef\u8b80\u53d6\u57e0` \u6216 `\u9060\u7aef IP \u4f4d\u5740`\u3002", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4", + "write": "\u5beb\u5165\u8acb\u6c42\u767c\u751f\u932f\u8aa4\uff0c\u8acb\u78ba\u8a8d `\u9060\u7aef\u5beb\u5165\u57e0` \u6216 `\u9060\u7aef IP \u4f4d\u5740`\u3002" + }, + "step": { + "user": { + "data": { + "ip_address": "\u9060\u7aef IP \u4f4d\u5740", + "listening_port": "\u672c\u5730\u76e3\u807d\u901a\u8a0a\u57e0", + "remote_read_port": "\u9060\u7aef\u8b80\u53d6\u57e0", + "remote_write_port": "\u9060\u7aef\u5beb\u5165\u57e0" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nightscout/sensor.py b/homeassistant/components/nightscout/sensor.py index 4ee75f66959..e8aaa11f23d 100644 --- a/homeassistant/components/nightscout/sensor.py +++ b/homeassistant/components/nightscout/sensor.py @@ -40,71 +40,40 @@ class NightscoutSensor(SensorEntity): def __init__(self, api: NightscoutAPI, name, unique_id): """Initialize the Nightscout sensor.""" self.api = api - self._unique_id = unique_id - self._name = name - self._state = None - self._attributes: dict[str, Any] = {} - self._unit_of_measurement = "mg/dL" - self._icon = "mdi:cloud-question" - self._available = False + self._attr_unique_id = unique_id + self._attr_name = name + self._attr_extra_state_attributes: dict[str, Any] = {} + self._attr_native_unit_of_measurement = "mg/dL" + self._attr_icon = "mdi:cloud-question" + self._attr_available = False - @property - def unique_id(self): - """Return the unique ID of the sensor.""" - return self._unique_id - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def native_unit_of_measurement(self): - """Return the unit the value is expressed in.""" - return self._unit_of_measurement - - @property - def available(self): - """Return if the sensor data are available.""" - return self._available - - @property - def native_value(self): - """Return the state of the device.""" - return self._state - - @property - def icon(self): - """Return the icon to use in the frontend, if any.""" - return self._icon - - async def async_update(self): + async def async_update(self) -> None: """Fetch the latest data from Nightscout REST API and update the state.""" try: values = await self.api.get_sgvs() except (ClientError, AsyncIOTimeoutError, OSError) as error: _LOGGER.error("Error fetching data. Failed with %s", error) - self._available = False + self._attr_available = False return - self._available = True - self._attributes = {} - self._state = None + self._attr_available = True + self._attr_extra_state_attributes = {} + self._attr_native_value = None if values: value = values[0] - self._attributes = { + self._attr_extra_state_attributes = { ATTR_DEVICE: value.device, ATTR_DATE: value.date, ATTR_DELTA: value.delta, ATTR_DIRECTION: value.direction, } - self._state = value.sgv - self._icon = self._parse_icon() + self._attr_native_value = value.sgv + self._attr_icon = self._parse_icon(value.direction) else: - self._available = False + self._attr_available = False _LOGGER.warning("Empty reply found when expecting JSON data") - def _parse_icon(self) -> str: + def _parse_icon(self, direction: str) -> str: """Update the icon based on the direction attribute.""" switcher = { "Flat": "mdi:arrow-right", @@ -115,9 +84,4 @@ class NightscoutSensor(SensorEntity): "FortyFiveUp": "mdi:arrow-top-right", "DoubleUp": "mdi:chevron-triple-up", } - return switcher.get(self._attributes[ATTR_DIRECTION], "mdi:cloud-question") - - @property - def extra_state_attributes(self): - """Return the state attributes.""" - return self._attributes + return switcher.get(direction, "mdi:cloud-question") diff --git a/homeassistant/components/nina/config_flow.py b/homeassistant/components/nina/config_flow.py index aa06b00e0ad..bdaf164fadb 100644 --- a/homeassistant/components/nina/config_flow.py +++ b/homeassistant/components/nina/config_flow.py @@ -153,7 +153,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback - def async_get_options_flow(config_entry): + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> OptionsFlowHandler: """Get the options flow for this handler.""" return OptionsFlowHandler(config_entry) diff --git a/homeassistant/components/nina/translations/bg.json b/homeassistant/components/nina/translations/bg.json index 7520f3a8624..be3ffecd284 100644 --- a/homeassistant/components/nina/translations/bg.json +++ b/homeassistant/components/nina/translations/bg.json @@ -7,5 +7,12 @@ "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" } + }, + "options": { + "step": { + "init": { + "title": "\u041e\u043f\u0446\u0438\u0438" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/nina/translations/cs.json b/homeassistant/components/nina/translations/cs.json index 66814b1652e..fbe3187769a 100644 --- a/homeassistant/components/nina/translations/cs.json +++ b/homeassistant/components/nina/translations/cs.json @@ -7,5 +7,11 @@ "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" } + }, + "options": { + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + } } } \ No newline at end of file diff --git a/homeassistant/components/nmap_tracker/__init__.py b/homeassistant/components/nmap_tracker/__init__.py index 5f3333ec750..e3b83f8abc2 100644 --- a/homeassistant/components/nmap_tracker/__init__.py +++ b/homeassistant/components/nmap_tracker/__init__.py @@ -14,7 +14,7 @@ from getmac import get_mac_address from mac_vendor_lookup import AsyncMacLookup from nmap import PortScanner, PortScannerError -from homeassistant.components.device_tracker.const import ( +from homeassistant.components.device_tracker import ( CONF_CONSIDER_HOME, CONF_SCAN_INTERVAL, DEFAULT_CONSIDER_HOME, diff --git a/homeassistant/components/nmap_tracker/config_flow.py b/homeassistant/components/nmap_tracker/config_flow.py index 42e84af2eaa..a1afa1b1bba 100644 --- a/homeassistant/components/nmap_tracker/config_flow.py +++ b/homeassistant/components/nmap_tracker/config_flow.py @@ -8,12 +8,12 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.components import network -from homeassistant.components.device_tracker.const import ( +from homeassistant.components.device_tracker import ( CONF_CONSIDER_HOME, CONF_SCAN_INTERVAL, DEFAULT_CONSIDER_HOME, ) -from homeassistant.components.network.const import MDNS_TARGET_IP +from homeassistant.components.network import MDNS_TARGET_IP from homeassistant.config_entries import ConfigEntry, OptionsFlow from homeassistant.const import CONF_EXCLUDE, CONF_HOSTS from homeassistant.core import HomeAssistant, callback diff --git a/homeassistant/components/nmap_tracker/translations/ja.json b/homeassistant/components/nmap_tracker/translations/ja.json index 60de8477859..75788b4b908 100644 --- a/homeassistant/components/nmap_tracker/translations/ja.json +++ b/homeassistant/components/nmap_tracker/translations/ja.json @@ -9,9 +9,9 @@ "step": { "user": { "data": { - "exclude": "\u30b9\u30ad\u30e3\u30f3\u5bfe\u8c61\u304b\u3089\u9664\u5916\u3059\u308b\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u30a2\u30c9\u30ec\u30b9(\u30b3\u30f3\u30de\u533a\u5207\u308a)", + "exclude": "\u30b9\u30ad\u30e3\u30f3\u304b\u3089\u9664\u5916\u3059\u308b\u30cd\u30c3\u30c8\u30ef\u30fc\u30af \u30a2\u30c9\u30ec\u30b9 (\u30ab\u30f3\u30de\u533a\u5207\u308a)", "home_interval": "\u30a2\u30af\u30c6\u30a3\u30d6\u306a\u30c7\u30d0\u30a4\u30b9\u306e\u30b9\u30ad\u30e3\u30f3\u9593\u9694(\u5206)\u306e\u6700\u5c0f\u6642\u9593(\u30d0\u30c3\u30c6\u30ea\u30fc\u3092\u7bc0\u7d04)", - "hosts": "\u30b9\u30ad\u30e3\u30f3\u3059\u308b\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u30a2\u30c9\u30ec\u30b9(\u30b3\u30f3\u30de\u533a\u5207\u308a)", + "hosts": "\u30b9\u30ad\u30e3\u30f3\u3059\u308b\u30cd\u30c3\u30c8\u30ef\u30fc\u30af \u30a2\u30c9\u30ec\u30b9 (\u30ab\u30f3\u30de\u533a\u5207\u308a)", "scan_options": "Nmap\u306b\u672a\u52a0\u5de5\u3067\u305d\u306e\u307e\u307e\u6e21\u3055\u308c\u308b\u30b9\u30ad\u30e3\u30f3\u8a2d\u5b9a\u306e\u30aa\u30d7\u30b7\u30e7\u30f3" }, "description": "Nmap\u3067\u30b9\u30ad\u30e3\u30f3\u3055\u308c\u308b\u30db\u30b9\u30c8\u3092\u8a2d\u5b9a\u3057\u307e\u3059\u3002\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u30a2\u30c9\u30ec\u30b9\u304a\u3088\u3073\u9664\u5916\u5bfe\u8c61\u306f\u3001IP\u30a2\u30c9\u30ec\u30b9(192.168.1.1)\u3001IP\u30cd\u30c3\u30c8\u30ef\u30fc\u30af(192.168.0.0/24)\u3001\u307e\u305f\u306f\u3001IP\u7bc4\u56f2(192.168.1.0-32)\u3067\u3059\u3002" @@ -26,9 +26,9 @@ "init": { "data": { "consider_home": "\u898b\u3048\u306a\u304f\u306a\u3063\u305f\u5f8c\u3001\u30c7\u30d0\u30a4\u30b9\u30c8\u30e9\u30c3\u30ab\u30fc\u3092\u30db\u30fc\u30e0\u3067\u306a\u3044\u3082\u306e\u3068\u3057\u3066\u898b\u306a\u3057\u3066\u3001\u30de\u30fc\u30af\u3059\u308b\u307e\u3067\u5f85\u6a5f\u3059\u308b\u307e\u3067\u306e\u79d2\u6570\u3002", - "exclude": "\u30b9\u30ad\u30e3\u30f3\u5bfe\u8c61\u304b\u3089\u9664\u5916\u3059\u308b\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u30a2\u30c9\u30ec\u30b9(\u30b3\u30f3\u30de\u533a\u5207\u308a)", + "exclude": "\u30b9\u30ad\u30e3\u30f3\u304b\u3089\u9664\u5916\u3059\u308b\u30cd\u30c3\u30c8\u30ef\u30fc\u30af \u30a2\u30c9\u30ec\u30b9 (\u30ab\u30f3\u30de\u533a\u5207\u308a)", "home_interval": "\u30a2\u30af\u30c6\u30a3\u30d6\u306a\u30c7\u30d0\u30a4\u30b9\u306e\u30b9\u30ad\u30e3\u30f3\u9593\u9694(\u5206)\u306e\u6700\u5c0f\u6642\u9593(\u30d0\u30c3\u30c6\u30ea\u30fc\u3092\u7bc0\u7d04)", - "hosts": "\u30b9\u30ad\u30e3\u30f3\u3059\u308b\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u30a2\u30c9\u30ec\u30b9(\u30b3\u30f3\u30de\u533a\u5207\u308a)", + "hosts": "\u30b9\u30ad\u30e3\u30f3\u3059\u308b\u30cd\u30c3\u30c8\u30ef\u30fc\u30af \u30a2\u30c9\u30ec\u30b9 (\u30ab\u30f3\u30de\u533a\u5207\u308a)", "interval_seconds": "\u30b9\u30ad\u30e3\u30f3\u9593\u9694", "scan_options": "Nmap\u306b\u672a\u52a0\u5de5\u3067\u305d\u306e\u307e\u307e\u6e21\u3055\u308c\u308b\u30b9\u30ad\u30e3\u30f3\u8a2d\u5b9a\u306e\u30aa\u30d7\u30b7\u30e7\u30f3" }, diff --git a/homeassistant/components/nmbs/sensor.py b/homeassistant/components/nmbs/sensor.py index fdb03652756..56fa0cd4a8d 100644 --- a/homeassistant/components/nmbs/sensor.py +++ b/homeassistant/components/nmbs/sensor.py @@ -158,7 +158,7 @@ class NMBSLiveBoard(SensorEntity): return attrs - def update(self): + def update(self) -> None: """Set the state equal to the next departure.""" liveboard = self._api_client.get_liveboard(self._station) @@ -278,7 +278,7 @@ class NMBSSensor(SensorEntity): return "vias" in self._attrs and int(self._attrs["vias"]["number"]) > 0 - def update(self): + def update(self) -> None: """Set the state to the duration of a connection.""" connections = self._api_client.get_connections( self._station_from, self._station_to diff --git a/homeassistant/components/noaa_tides/sensor.py b/homeassistant/components/noaa_tides/sensor.py index 6e398fa7183..49635973cf8 100644 --- a/homeassistant/components/noaa_tides/sensor.py +++ b/homeassistant/components/noaa_tides/sensor.py @@ -130,7 +130,7 @@ class NOAATidesAndCurrentsSensor(SensorEntity): return f"Low tide at {tidetime}" return None - def update(self): + def update(self) -> None: """Get the latest data from NOAA Tides and Currents API.""" begin = datetime.now() delta = timedelta(days=2) diff --git a/homeassistant/components/nobo_hub/__init__.py b/homeassistant/components/nobo_hub/__init__.py new file mode 100644 index 00000000000..7db9eb96f7e --- /dev/null +++ b/homeassistant/components/nobo_hub/__init__.py @@ -0,0 +1,86 @@ +"""The Nobø Ecohub integration.""" +from __future__ import annotations + +import logging + +from pynobo import nobo + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + ATTR_NAME, + CONF_IP_ADDRESS, + EVENT_HOMEASSISTANT_STOP, + Platform, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry + +from .const import ( + ATTR_HARDWARE_VERSION, + ATTR_SERIAL, + ATTR_SOFTWARE_VERSION, + CONF_AUTO_DISCOVERED, + CONF_SERIAL, + DOMAIN, + NOBO_MANUFACTURER, +) + +PLATFORMS = [Platform.CLIMATE] + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Nobø Ecohub from a config entry.""" + + serial = entry.data[CONF_SERIAL] + discover = entry.data[CONF_AUTO_DISCOVERED] + ip_address = None if discover else entry.data[CONF_IP_ADDRESS] + hub = nobo(serial=serial, ip=ip_address, discover=discover, synchronous=False) + await hub.start() + + hass.data.setdefault(DOMAIN, {}) + + # Register hub as device + dev_reg = device_registry.async_get(hass) + dev_reg.async_get_or_create( + config_entry_id=entry.entry_id, + identifiers={(DOMAIN, hub.hub_info[ATTR_SERIAL])}, + manufacturer=NOBO_MANUFACTURER, + name=hub.hub_info[ATTR_NAME], + model=f"Nobø Ecohub ({hub.hub_info[ATTR_HARDWARE_VERSION]})", + sw_version=hub.hub_info[ATTR_SOFTWARE_VERSION], + ) + + async def _async_close(event): + """Close the Nobø Ecohub socket connection when HA stops.""" + await hub.stop() + + entry.async_on_unload( + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_close) + ) + hass.data[DOMAIN][entry.entry_id] = hub + + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + + entry.async_on_unload(entry.add_update_listener(options_update_listener)) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + + hub: nobo = hass.data[DOMAIN][entry.entry_id] + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + await hub.stop() + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok + + +async def options_update_listener( + hass: HomeAssistant, config_entry: ConfigEntry +) -> None: + """Handle options update.""" + await hass.config_entries.async_reload(config_entry.entry_id) diff --git a/homeassistant/components/nobo_hub/climate.py b/homeassistant/components/nobo_hub/climate.py new file mode 100644 index 00000000000..a465bfa77ab --- /dev/null +++ b/homeassistant/components/nobo_hub/climate.py @@ -0,0 +1,209 @@ +"""Python Control of Nobø Hub - Nobø Energy Control.""" +from __future__ import annotations + +import logging +from typing import Any + +from pynobo import nobo + +from homeassistant.components.climate import ( + ATTR_TARGET_TEMP_HIGH, + ATTR_TARGET_TEMP_LOW, + PRESET_AWAY, + PRESET_COMFORT, + PRESET_ECO, + PRESET_NONE, + ClimateEntity, + ClimateEntityFeature, + HVACMode, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + ATTR_IDENTIFIERS, + ATTR_MODE, + ATTR_NAME, + ATTR_SUGGESTED_AREA, + ATTR_VIA_DEVICE, + PRECISION_WHOLE, + TEMP_CELSIUS, +) +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import ( + ATTR_OVERRIDE_ALLOWED, + ATTR_SERIAL, + ATTR_TARGET_ID, + ATTR_TARGET_TYPE, + ATTR_TEMP_COMFORT_C, + ATTR_TEMP_ECO_C, + CONF_OVERRIDE_TYPE, + DOMAIN, + OVERRIDE_TYPE_NOW, +) + +SUPPORT_FLAGS = ( + ClimateEntityFeature.PRESET_MODE | ClimateEntityFeature.TARGET_TEMPERATURE_RANGE +) + +PRESET_MODES = [PRESET_NONE, PRESET_COMFORT, PRESET_ECO, PRESET_AWAY] + +MIN_TEMPERATURE = 7 +MAX_TEMPERATURE = 40 + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Nobø Ecohub platform from UI configuration.""" + + # Setup connection with hub + hub: nobo = hass.data[DOMAIN][config_entry.entry_id] + + override_type = ( + nobo.API.OVERRIDE_TYPE_NOW + if config_entry.options.get(CONF_OVERRIDE_TYPE) == OVERRIDE_TYPE_NOW + else nobo.API.OVERRIDE_TYPE_CONSTANT + ) + + # Add zones as entities + async_add_entities( + [NoboZone(zone_id, hub, override_type) for zone_id in hub.zones], + True, + ) + + +class NoboZone(ClimateEntity): + """Representation of a Nobø zone. + + A Nobø zone consists of a group of physical devices that are + controlled as a unity. + """ + + _attr_max_temp = MAX_TEMPERATURE + _attr_min_temp = MIN_TEMPERATURE + _attr_precision = PRECISION_WHOLE + _attr_preset_modes = PRESET_MODES + # Need to poll to get preset change when in HVACMode.AUTO. + _attr_supported_features = SUPPORT_FLAGS + _attr_temperature_unit = TEMP_CELSIUS + + def __init__(self, zone_id, hub: nobo, override_type): + """Initialize the climate device.""" + self._id = zone_id + self._nobo = hub + self._attr_unique_id = f"{hub.hub_serial}:{zone_id}" + self._attr_name = None + self._attr_has_entity_name = True + self._attr_hvac_mode = HVACMode.AUTO + self._attr_hvac_modes = [HVACMode.HEAT, HVACMode.AUTO] + self._override_type = override_type + self._attr_device_info: DeviceInfo = { + ATTR_IDENTIFIERS: {(DOMAIN, f"{hub.hub_serial}:{zone_id}")}, + ATTR_NAME: hub.zones[zone_id][ATTR_NAME], + ATTR_VIA_DEVICE: (DOMAIN, hub.hub_info[ATTR_SERIAL]), + ATTR_SUGGESTED_AREA: hub.zones[zone_id][ATTR_NAME], + } + + async def async_added_to_hass(self) -> None: + """Register callback from hub.""" + self._nobo.register_callback(self._after_update) + + async def async_will_remove_from_hass(self) -> None: + """Deregister callback from hub.""" + self._nobo.deregister_callback(self._after_update) + + async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: + """Set new target HVAC mode, if it's supported.""" + if hvac_mode not in self.hvac_modes: + raise ValueError( + f"Zone {self._id} '{self._attr_name}' called with unsupported HVAC mode '{hvac_mode}'" + ) + if hvac_mode == HVACMode.AUTO: + await self.async_set_preset_mode(PRESET_NONE) + elif hvac_mode == HVACMode.HEAT: + await self.async_set_preset_mode(PRESET_COMFORT) + self._attr_hvac_mode = hvac_mode + + async def async_set_preset_mode(self, preset_mode: str) -> None: + """Set new zone override.""" + if self._nobo.zones[self._id][ATTR_OVERRIDE_ALLOWED] != "1": + return + if preset_mode == PRESET_ECO: + mode = nobo.API.OVERRIDE_MODE_ECO + elif preset_mode == PRESET_AWAY: + mode = nobo.API.OVERRIDE_MODE_AWAY + elif preset_mode == PRESET_COMFORT: + mode = nobo.API.OVERRIDE_MODE_COMFORT + else: # PRESET_NONE + mode = nobo.API.OVERRIDE_MODE_NORMAL + await self._nobo.async_create_override( + mode, + self._override_type, + nobo.API.OVERRIDE_TARGET_ZONE, + self._id, + ) + + async def async_set_temperature(self, **kwargs: Any) -> None: + """Set new target temperature.""" + if ATTR_TARGET_TEMP_LOW in kwargs: + low = round(kwargs[ATTR_TARGET_TEMP_LOW]) + high = round(kwargs[ATTR_TARGET_TEMP_HIGH]) + low = min(low, high) + high = max(low, high) + await self._nobo.async_update_zone( + self._id, temp_comfort_c=high, temp_eco_c=low + ) + + async def async_update(self) -> None: + """Fetch new state data for this zone.""" + self._read_state() + + @callback + def _read_state(self) -> None: + """Read the current state from the hub. These are only local calls.""" + state = self._nobo.get_current_zone_mode(self._id) + self._attr_hvac_mode = HVACMode.AUTO + self._attr_preset_mode = PRESET_NONE + + if state == nobo.API.NAME_OFF: + self._attr_hvac_mode = HVACMode.OFF + elif state == nobo.API.NAME_AWAY: + self._attr_preset_mode = PRESET_AWAY + elif state == nobo.API.NAME_ECO: + self._attr_preset_mode = PRESET_ECO + elif state == nobo.API.NAME_COMFORT: + self._attr_preset_mode = PRESET_COMFORT + + if self._nobo.zones[self._id][ATTR_OVERRIDE_ALLOWED] == "1": + for override in self._nobo.overrides: + if self._nobo.overrides[override][ATTR_MODE] == "0": + continue # "normal" overrides + if ( + self._nobo.overrides[override][ATTR_TARGET_TYPE] + == nobo.API.OVERRIDE_TARGET_ZONE + and self._nobo.overrides[override][ATTR_TARGET_ID] == self._id + ): + self._attr_hvac_mode = HVACMode.HEAT + break + + current_temperature = self._nobo.get_current_zone_temperature(self._id) + self._attr_current_temperature = ( + None if current_temperature is None else float(current_temperature) + ) + self._attr_target_temperature_high = int( + self._nobo.zones[self._id][ATTR_TEMP_COMFORT_C] + ) + self._attr_target_temperature_low = int( + self._nobo.zones[self._id][ATTR_TEMP_ECO_C] + ) + + @callback + def _after_update(self, hub): + self._read_state() + self.async_write_ha_state() diff --git a/homeassistant/components/nobo_hub/config_flow.py b/homeassistant/components/nobo_hub/config_flow.py new file mode 100644 index 00000000000..f1e2dd7d9d2 --- /dev/null +++ b/homeassistant/components/nobo_hub/config_flow.py @@ -0,0 +1,210 @@ +"""Config flow for Nobø Ecohub integration.""" +from __future__ import annotations + +import socket +from typing import Any + +from pynobo import nobo +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_IP_ADDRESS +from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult +from homeassistant.exceptions import HomeAssistantError + +from .const import ( + CONF_AUTO_DISCOVERED, + CONF_OVERRIDE_TYPE, + CONF_SERIAL, + DOMAIN, + OVERRIDE_TYPE_CONSTANT, + OVERRIDE_TYPE_NOW, +) + +DATA_NOBO_HUB_IMPL = "nobo_hub_flow_implementation" +DEVICE_INPUT = "device_input" + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Nobø Ecohub.""" + + VERSION = 1 + + def __init__(self): + """Initialize the config flow.""" + self._discovered_hubs = None + self._hub = None + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the initial step.""" + if self._discovered_hubs is None: + self._discovered_hubs = dict(await nobo.async_discover_hubs()) + + if not self._discovered_hubs: + # No hubs auto discovered + return await self.async_step_manual() + + if user_input is not None: + if user_input["device"] == "manual": + return await self.async_step_manual() + self._hub = user_input["device"] + return await self.async_step_selected() + + hubs = self._hubs() + hubs["manual"] = "Manual" + data_schema = vol.Schema( + { + vol.Required("device"): vol.In(hubs), + } + ) + return self.async_show_form( + step_id="user", + data_schema=data_schema, + ) + + async def async_step_selected( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle configuration of a selected discovered device.""" + errors = {} + if user_input is not None: + serial_prefix = self._discovered_hubs[self._hub] + serial_suffix = user_input["serial_suffix"] + serial = f"{serial_prefix}{serial_suffix}" + try: + return await self._create_configuration(serial, self._hub, True) + except NoboHubConnectError as error: + errors["base"] = error.msg + + user_input = user_input or {} + return self.async_show_form( + step_id="selected", + data_schema=vol.Schema( + { + vol.Required( + "serial_suffix", default=user_input.get("serial_suffix") + ): str, + } + ), + errors=errors, + description_placeholders={ + "hub": self._format_hub(self._hub, self._discovered_hubs[self._hub]) + }, + ) + + async def async_step_manual( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle configuration of an undiscovered device.""" + errors = {} + if user_input is not None: + serial = user_input[CONF_SERIAL] + ip_address = user_input[CONF_IP_ADDRESS] + try: + return await self._create_configuration(serial, ip_address, False) + except NoboHubConnectError as error: + errors["base"] = error.msg + + user_input = user_input or {} + return self.async_show_form( + step_id="manual", + data_schema=vol.Schema( + { + vol.Required(CONF_SERIAL, default=user_input.get(CONF_SERIAL)): str, + vol.Required( + CONF_IP_ADDRESS, default=user_input.get(CONF_IP_ADDRESS) + ): str, + } + ), + errors=errors, + ) + + async def _create_configuration( + self, serial: str, ip_address: str, auto_discovered: bool + ) -> FlowResult: + await self.async_set_unique_id(serial) + self._abort_if_unique_id_configured() + name = await self._test_connection(serial, ip_address) + return self.async_create_entry( + title=name, + data={ + CONF_SERIAL: serial, + CONF_IP_ADDRESS: ip_address, + CONF_AUTO_DISCOVERED: auto_discovered, + }, + ) + + async def _test_connection(self, serial: str, ip_address: str) -> str: + if not len(serial) == 12 or not serial.isdigit(): + raise NoboHubConnectError("invalid_serial") + try: + socket.inet_aton(ip_address) + except OSError as err: + raise NoboHubConnectError("invalid_ip") from err + hub = nobo(serial=serial, ip=ip_address, discover=False, synchronous=False) + if not await hub.async_connect_hub(ip_address, serial): + raise NoboHubConnectError("cannot_connect") + name = hub.hub_info["name"] + await hub.close() + return name + + @staticmethod + def _format_hub(ip, serial_prefix): + return f"{serial_prefix}XXX ({ip})" + + def _hubs(self): + return { + ip: self._format_hub(ip, serial_prefix) + for ip, serial_prefix in self._discovered_hubs.items() + } + + @staticmethod + @callback + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> config_entries.OptionsFlow: + """Get the options flow for this handler.""" + return OptionsFlowHandler(config_entry) + + +class NoboHubConnectError(HomeAssistantError): + """Error with connecting to Nobø Ecohub.""" + + def __init__(self, msg) -> None: + """Instantiate error.""" + super().__init__() + self.msg = msg + + +class OptionsFlowHandler(config_entries.OptionsFlow): + """Handles options flow for the component.""" + + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + """Initialize the options flow.""" + self.config_entry = config_entry + + async def async_step_init(self, user_input=None) -> FlowResult: + """Manage the options.""" + + if user_input is not None: + data = { + CONF_OVERRIDE_TYPE: user_input.get(CONF_OVERRIDE_TYPE), + } + return self.async_create_entry(title="", data=data) + + override_type = self.config_entry.options.get( + CONF_OVERRIDE_TYPE, OVERRIDE_TYPE_CONSTANT + ) + + schema = vol.Schema( + { + vol.Required(CONF_OVERRIDE_TYPE, default=override_type): vol.In( + [OVERRIDE_TYPE_CONSTANT, OVERRIDE_TYPE_NOW] + ), + } + ) + + return self.async_show_form(step_id="init", data_schema=schema) diff --git a/homeassistant/components/nobo_hub/const.py b/homeassistant/components/nobo_hub/const.py new file mode 100644 index 00000000000..320c2f43c07 --- /dev/null +++ b/homeassistant/components/nobo_hub/const.py @@ -0,0 +1,19 @@ +"""Constants for the Nobø Ecohub integration.""" + +DOMAIN = "nobo_hub" + +CONF_AUTO_DISCOVERED = "auto_discovered" +CONF_SERIAL = "serial" +CONF_OVERRIDE_TYPE = "override_type" +OVERRIDE_TYPE_CONSTANT = "Constant" +OVERRIDE_TYPE_NOW = "Now" + +NOBO_MANUFACTURER = "Glen Dimplex Nordic AS" +ATTR_HARDWARE_VERSION = "hardware_version" +ATTR_SOFTWARE_VERSION = "software_version" +ATTR_SERIAL = "serial" +ATTR_TEMP_COMFORT_C = "temp_comfort_c" +ATTR_TEMP_ECO_C = "temp_eco_c" +ATTR_OVERRIDE_ALLOWED = "override_allowed" +ATTR_TARGET_TYPE = "target_type" +ATTR_TARGET_ID = "target_id" diff --git a/homeassistant/components/nobo_hub/manifest.json b/homeassistant/components/nobo_hub/manifest.json new file mode 100644 index 00000000000..14e10a1ffaf --- /dev/null +++ b/homeassistant/components/nobo_hub/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "nobo_hub", + "name": "Nob\u00f8 Ecohub", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/nobo_hub", + "requirements": ["pynobo==1.4.0"], + "codeowners": ["@echoromeo", "@oyvindwe"], + "iot_class": "local_push" +} diff --git a/homeassistant/components/nobo_hub/strings.json b/homeassistant/components/nobo_hub/strings.json new file mode 100644 index 00000000000..cfa339c98df --- /dev/null +++ b/homeassistant/components/nobo_hub/strings.json @@ -0,0 +1,44 @@ +{ + "config": { + "step": { + "user": { + "description": "Select Nobø Ecohub to configure.", + "data": { + "device": "Discovered hubs" + } + }, + "selected": { + "description": "Configuring {hub}.\r\rTo connect to the hub, you need to enter the last 3 digits of the hub's serial number.", + "data": { + "serial_suffix": "Serial number suffix (3 digits)" + } + }, + "manual": { + "description": "Configure a Nobø Ecohub not discovered on your local network. If your hub is on another network, you can still connect to it by entering the complete serial number (12 digits) and its IP address.", + "data": { + "serial": "Serial number (12 digits)", + "ip_address": "[%key:common::config_flow::data::ip%]" + } + } + }, + "error": { + "cannot_connect": "Failed to connect - check serial number", + "invalid_serial": "Invalid serial number", + "invalid_ip": "Invalid IP address", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + }, + "options": { + "step": { + "init": { + "data": { + "override_type": "Override type" + }, + "description": "Select override type \"Now\" to end override on next week profile change." + } + } + } +} diff --git a/homeassistant/components/nobo_hub/translations/bg.json b/homeassistant/components/nobo_hub/translations/bg.json new file mode 100644 index 00000000000..88f263dc963 --- /dev/null +++ b/homeassistant/components/nobo_hub/translations/bg.json @@ -0,0 +1,33 @@ +{ + "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 - \u043f\u0440\u043e\u0432\u0435\u0440\u0435\u0442\u0435 \u0441\u0435\u0440\u0438\u0439\u043d\u0438\u044f \u043d\u043e\u043c\u0435\u0440", + "invalid_ip": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d IP \u0430\u0434\u0440\u0435\u0441", + "invalid_serial": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d \u0441\u0435\u0440\u0438\u0435\u043d \u043d\u043e\u043c\u0435\u0440", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "manual": { + "data": { + "ip_address": "IP \u0430\u0434\u0440\u0435\u0441", + "serial": "\u0421\u0435\u0440\u0438\u0435\u043d \u043d\u043e\u043c\u0435\u0440 (12 \u0446\u0438\u0444\u0440\u0438)" + } + }, + "selected": { + "data": { + "serial_suffix": "\u0421\u0443\u0444\u0438\u043a\u0441 \u043d\u0430 \u0441\u0435\u0440\u0438\u0439\u043d\u0438\u044f \u043d\u043e\u043c\u0435\u0440 (3 \u0446\u0438\u0444\u0440\u0438)" + }, + "description": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 {hub}.\n\n\u0417\u0430 \u0434\u0430 \u0441\u0435 \u0441\u0432\u044a\u0440\u0436\u0435\u0442\u0435 \u0441 \u0445\u044a\u0431\u0430, \u0442\u0440\u044f\u0431\u0432\u0430 \u0434\u0430 \u0432\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u043f\u043e\u0441\u043b\u0435\u0434\u043d\u0438\u0442\u0435 3 \u0446\u0438\u0444\u0440\u0438 \u043e\u0442 \u0441\u0435\u0440\u0438\u0439\u043d\u0438\u044f \u043c\u0443 \u043d\u043e\u043c\u0435\u0440." + }, + "user": { + "data": { + "device": "\u041e\u0442\u043a\u0440\u0438\u0442\u0438 \u0445\u044a\u0431\u043e\u0432\u0435" + }, + "description": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 Nob\u00f8 Ecohub \u0437\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0435." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nobo_hub/translations/bn.json b/homeassistant/components/nobo_hub/translations/bn.json new file mode 100644 index 00000000000..9c61784e29a --- /dev/null +++ b/homeassistant/components/nobo_hub/translations/bn.json @@ -0,0 +1,44 @@ +{ + "config": { + "abort": { + "already_configured": "\u09a1\u09bf\u09ad\u09be\u0987\u09b8 \u0987\u09a4\u09bf\u09ae\u09a7\u09cd\u09af\u09c7 \u0995\u09a8\u09ab\u09bf\u0997\u09be\u09b0 \u0995\u09b0\u09be \u0986\u099b\u09c7" + }, + "error": { + "cannot_connect": "\u09b8\u0982\u09af\u09cb\u0997 \u0995\u09b0\u09a4\u09c7 \u09ac\u09cd\u09af\u09b0\u09cd\u09a5 - \u0995\u09cd\u09b0\u09ae\u09bf\u0995 \u09b8\u0982\u0996\u09cd\u09af\u09be \u09aa\u09b0\u09c0\u0995\u09cd\u09b7\u09be \u0995\u09b0\u09c1\u09a8", + "invalid_ip": "\u0985\u0995\u09be\u09b0\u09cd\u09af\u0995\u09b0 \u0986\u0987\u09aa\u09bf \u09a0\u09bf\u0995\u09be\u09a8\u09be", + "invalid_serial": "\u0985\u0995\u09be\u09b0\u09cd\u09af\u0995\u09b0 \u0995\u09cd\u09b0\u09ae\u09bf\u0995 \u09b8\u0982\u0996\u09cd\u09af\u09be", + "unknown": "\u0985\u09aa\u09cd\u09b0\u09a4\u09cd\u09af\u09be\u09b6\u09bf\u09a4 \u09a4\u09cd\u09b0\u09c1\u099f\u09bf" + }, + "step": { + "manual": { + "data": { + "ip_address": "\u0986\u0987\u09aa\u09bf \u09a0\u09bf\u0995\u09be\u09a8\u09be", + "serial": "\u0995\u09cd\u09b0\u09ae\u09bf\u0995 \u09b8\u0982\u0996\u09cd\u09af\u09be (\u09e7\u09e8 \u099f\u09bf \u09b8\u0982\u0996\u09cd\u09af\u09be)" + }, + "description": "\u0986\u09aa\u09a8\u09be\u09b0 \u09b8\u09cd\u09a5\u09be\u09a8\u09c0\u09af\u09bc \u09a8\u09c7\u099f\u0993\u09af\u09bc\u09be\u09b0\u09cd\u0995\u09c7 \u0986\u09ac\u09bf\u09b7\u09cd\u0995\u09c3\u09a4 \u09a8\u09af\u09bc \u098f\u09ae\u09a8 \u098f\u0995\u099f\u09bf \u09a8\u09cb\u09ac\u09cb \u0987\u0995\u09cb\u09b9\u09be\u09ac \u0995\u09a8\u09ab\u09bf\u0997\u09be\u09b0 \u0995\u09b0\u09c1\u09a8\u0964 \u09af\u09a6\u09bf \u0986\u09aa\u09a8\u09be\u09b0 \u09b9\u09be\u09ac\u099f\u09bf \u0985\u09a8\u09cd\u09af \u09a8\u09c7\u099f\u0993\u09af\u09bc\u09be\u09b0\u09cd\u0995\u09c7 \u09a5\u09be\u0995\u09c7 \u09a4\u09ac\u09c7 \u0986\u09aa\u09a8\u09bf \u098f\u0996\u09a8\u0993 \u09b8\u09ae\u09cd\u09aa\u09c2\u09b0\u09cd\u09a3 \u0995\u09cd\u09b0\u09ae\u09bf\u0995 \u09a8\u09ae\u09cd\u09ac\u09b0 (\u09e7\u09e8 \u099f\u09bf \u09b8\u0982\u0996\u09cd\u09af\u09be) \u098f\u09ac\u0982 \u098f\u09b0 \u0986\u0987\u09aa\u09bf \u09a0\u09bf\u0995\u09be\u09a8\u09be \u09b2\u09bf\u0996\u09c7 \u098f\u099f\u09bf\u09b0 \u09b8\u09be\u09a5\u09c7 \u09b8\u0982\u09af\u09cb\u0997 \u0995\u09b0\u09a4\u09c7 \u09aa\u09be\u09b0\u09c7\u09a8\u0964" + }, + "selected": { + "data": { + "serial_suffix": "\u0995\u09cd\u09b0\u09ae\u09bf\u0995 \u09b8\u0982\u0996\u09cd\u09af\u09be \u09aa\u09cd\u09b0\u09a4\u09cd\u09af\u09af\u09bc (\u09e9 \u099f\u09bf \u09b8\u0982\u0996\u09cd\u09af\u09be)" + }, + "description": "{hub} \u0995\u09a8\u09ab\u09bf\u0997\u09be\u09b0 \u0995\u09b0\u09be \u09b9\u099a\u09cd\u099b\u09c7\u0964\n\n\u09b9\u09be\u09ac\u09c7\u09b0 \u09b8\u09be\u09a5\u09c7 \u09b8\u0982\u09af\u09cb\u0997 \u0995\u09b0\u09a4\u09c7, \u0986\u09aa\u09a8\u09be\u0995\u09c7 \u09b9\u09be\u09ac\u09c7\u09b0 \u0995\u09cd\u09b0\u09ae\u09bf\u0995 \u09a8\u09ae\u09cd\u09ac\u09b0\u09c7\u09b0 \u09b6\u09c7\u09b7 \u09e9 \u099f\u09bf \u09b8\u0982\u0996\u09cd\u09af\u09be \u09b2\u09bf\u0996\u09a4\u09c7 \u09b9\u09ac\u09c7\u0964" + }, + "user": { + "data": { + "device": "\u0986\u09ac\u09bf\u09b7\u09cd\u0995\u09c3\u09a4 \u09b9\u09be\u09ac" + }, + "description": "\u0995\u09a8\u09ab\u09bf\u0997\u09be\u09b0 \u0995\u09b0\u09a4\u09c7 Nob\u00f8 Ecohub \u09a8\u09bf\u09b0\u09cd\u09ac\u09be\u099a\u09a8 \u0995\u09b0\u09c1\u09a8\u0964" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "override_type": "\u0985\u0997\u09cd\u09b0\u09be\u09a7\u09bf\u0995\u09be\u09b0 \u09a7\u09b0\u09a3" + }, + "description": "\u09aa\u09b0\u09c7\u09b0 \u09b8\u09aa\u09cd\u09a4\u09be\u09b9\u09c7\u09b0 \u09aa\u09cd\u09b0\u09cb\u09ab\u09be\u0987\u09b2 \u09aa\u09b0\u09bf\u09ac\u09b0\u09cd\u09a4\u09a8\u09c7 \u0993\u09ad\u09be\u09b0\u09b0\u09be\u0987\u09a1 \u09b6\u09c7\u09b7 \u0995\u09b0\u09a4\u09c7 \u0993\u09ad\u09be\u09b0\u09b0\u09be\u0987\u09a1 \u099f\u09be\u0987\u09aa \"\u098f\u0996\u09a8\" \u09a8\u09bf\u09b0\u09cd\u09ac\u09be\u099a\u09a8 \u0995\u09b0\u09c1\u09a8\u0964" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nobo_hub/translations/ca.json b/homeassistant/components/nobo_hub/translations/ca.json new file mode 100644 index 00000000000..58f53e2e5af --- /dev/null +++ b/homeassistant/components/nobo_hub/translations/ca.json @@ -0,0 +1,44 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "error": { + "cannot_connect": "No s'ha pogut connectar - comprova el n\u00famero de s\u00e8rie", + "invalid_ip": "Adre\u00e7a IP inv\u00e0lida", + "invalid_serial": "N\u00famero de s\u00e8rie inv\u00e0lid", + "unknown": "Error inesperat" + }, + "step": { + "manual": { + "data": { + "ip_address": "Adre\u00e7a IP", + "serial": "N\u00famero de s\u00e8rie (12 d\u00edgits)" + }, + "description": "Configura un Nob\u00f8 Ecohub que no ha estat descobert a la teva xarxa local. Si el teu concentrador es troba en una altra xarxa, pots connectar-lo introduint el n\u00famero de s\u00e8rie complet (12 d\u00edgits) i la seva adre\u00e7a IP." + }, + "selected": { + "data": { + "serial_suffix": "Sufix del n\u00famero de s\u00e8rie (3 d\u00edgits)" + }, + "description": "Configurant {hub}.\n\nPer connectar-te al hub, has d'introduir els darrers 3 d\u00edgits del n\u00famero de s\u00e8rie del hub." + }, + "user": { + "data": { + "device": "Hubs descoberts" + }, + "description": "Selecciona el Nob\u00f8 Ecohub a configurar" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "override_type": "Tipus de substituci\u00f3" + }, + "description": "Selecciona substitueix el tipus \"Ara\" (\"Now\") per finalitzar la substituci\u00f3 del canvi de perfil de la setmana vinent." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nobo_hub/translations/cs.json b/homeassistant/components/nobo_hub/translations/cs.json new file mode 100644 index 00000000000..d5e80179cf6 --- /dev/null +++ b/homeassistant/components/nobo_hub/translations/cs.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit - zkontrolujte s\u00e9riov\u00e9 \u010d\u00edslo", + "invalid_ip": "Neplatn\u00e1 IP adresa", + "invalid_serial": "Neplatn\u00e9 s\u00e9riov\u00e9 \u010d\u00edslo", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "manual": { + "data": { + "ip_address": "IP adresa", + "serial": "S\u00e9riov\u00e9 \u010d\u00edslo (12 \u010d\u00edslic)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nobo_hub/translations/de.json b/homeassistant/components/nobo_hub/translations/de.json new file mode 100644 index 00000000000..f3c392c1f5b --- /dev/null +++ b/homeassistant/components/nobo_hub/translations/de.json @@ -0,0 +1,44 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen - Seriennummer pr\u00fcfen", + "invalid_ip": "Ung\u00fcltige IP-Adresse", + "invalid_serial": "Ung\u00fcltige Seriennummer", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "manual": { + "data": { + "ip_address": "IP-Adresse", + "serial": "Seriennummer (12 Ziffern)" + }, + "description": "Konfiguriere einen Nob\u00f8 Ecohub, der nicht in deinem lokalen Netzwerk gefunden wurde. Wenn sich dein Hub in einem anderen Netzwerk befindet, kannst du dich trotzdem mit ihm verbinden, indem du die vollst\u00e4ndige Seriennummer (12 Ziffern) und seine IP-Adresse eingibst." + }, + "selected": { + "data": { + "serial_suffix": "Seriennummern-Suffix (3 Ziffern)" + }, + "description": "Konfigurieren von {hub}. Um eine Verbindung zum Hub herzustellen, musst du die letzten 3 Ziffern der Seriennummer des Hubs eingeben." + }, + "user": { + "data": { + "device": "Entdeckte Hubs" + }, + "description": "W\u00e4hle den Nob\u00f8 Ecohub zum Konfigurieren." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "override_type": "Typ \u00fcberschreiben" + }, + "description": "W\u00e4hle die \u00dcberschreibungsart \"Jetzt\", um die \u00dcberschreibung bei der n\u00e4chsten Profil\u00e4nderung in der Woche zu beenden." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nobo_hub/translations/el.json b/homeassistant/components/nobo_hub/translations/el.json new file mode 100644 index 00000000000..946b841361e --- /dev/null +++ b/homeassistant/components/nobo_hub/translations/el.json @@ -0,0 +1,44 @@ +{ + "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\u03ad\u03c4\u03c5\u03c7\u03b5 \u03b7 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 - \u03b5\u03bb\u03ad\u03b3\u03be\u03c4\u03b5 \u03c4\u03bf\u03bd \u03c3\u03b5\u03b9\u03c1\u03b9\u03b1\u03ba\u03cc \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc", + "invalid_ip": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP", + "invalid_serial": "\u039c\u03b7 \u03ad\u03b3\u03b3\u03c5\u03c1\u03bf\u03c2 \u03c3\u03b5\u03b9\u03c1\u03b9\u03b1\u03ba\u03cc\u03c2 \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc\u03c2", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, + "step": { + "manual": { + "data": { + "ip_address": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP", + "serial": "\u03a3\u03b5\u03b9\u03c1\u03b9\u03b1\u03ba\u03cc\u03c2 \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc\u03c2 (12 \u03c8\u03b7\u03c6\u03af\u03b1)" + }, + "description": "\u0394\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03ce\u03c3\u03c4\u03b5 \u03ad\u03bd\u03b1 Nob\u00f8 Ecohub \u03c0\u03bf\u03c5 \u03b4\u03b5\u03bd \u03b1\u03bd\u03b1\u03ba\u03b1\u03bb\u03cd\u03c6\u03b8\u03b7\u03ba\u03b5 \u03c3\u03c4\u03bf \u03c4\u03bf\u03c0\u03b9\u03ba\u03cc \u03c3\u03b1\u03c2 \u03b4\u03af\u03ba\u03c4\u03c5\u03bf. \u0395\u03ac\u03bd \u03bf \u03b4\u03b9\u03b1\u03bd\u03bf\u03bc\u03ad\u03b1\u03c2 \u03c3\u03b1\u03c2 \u03b2\u03c1\u03af\u03c3\u03ba\u03b5\u03c4\u03b1\u03b9 \u03c3\u03b5 \u03ac\u03bb\u03bb\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03bf, \u03bc\u03c0\u03bf\u03c1\u03b5\u03af\u03c4\u03b5 \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af\u03c4\u03b5 \u03c3\u03b5 \u03b1\u03c5\u03c4\u03cc \u03b5\u03b9\u03c3\u03ac\u03b3\u03bf\u03bd\u03c4\u03b1\u03c2 \u03c4\u03bf\u03bd \u03c0\u03bb\u03ae\u03c1\u03b7 \u03c3\u03b5\u03b9\u03c1\u03b9\u03b1\u03ba\u03cc \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc (12 \u03c8\u03b7\u03c6\u03af\u03b1) \u03ba\u03b1\u03b9 \u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP \u03c4\u03bf\u03c5." + }, + "selected": { + "data": { + "serial_suffix": "\u0395\u03c0\u03af\u03b8\u03b7\u03bc\u03b1 \u03c3\u03b5\u03b9\u03c1\u03b9\u03b1\u03ba\u03bf\u03cd \u03b1\u03c1\u03b9\u03b8\u03bc\u03bf\u03cd (3 \u03c8\u03b7\u03c6\u03af\u03b1)" + }, + "description": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 {hub}. \n\n\u0393\u03b9\u03b1 \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03bf hub, \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b1 \u03c4\u03b5\u03bb\u03b5\u03c5\u03c4\u03b1\u03af\u03b1 3 \u03c8\u03b7\u03c6\u03af\u03b1 \u03c4\u03bf\u03c5 \u03c3\u03b5\u03b9\u03c1\u03b9\u03b1\u03ba\u03bf\u03cd \u03b1\u03c1\u03b9\u03b8\u03bc\u03bf\u03cd \u03c4\u03bf\u03c5 hub." + }, + "user": { + "data": { + "device": "\u0391\u03bd\u03b1\u03ba\u03b1\u03bb\u03c5\u03c6\u03b8\u03ad\u03bd\u03c4\u03b5\u03c2 \u03ba\u03cc\u03bc\u03b2\u03bf\u03b9" + }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 Nob\u00f8 Ecohub \u03b3\u03b9\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c0\u03b1\u03c1\u03b1\u03bc\u03ad\u03c4\u03c1\u03c9\u03bd." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "override_type": "\u03a4\u03cd\u03c0\u03bf\u03c2 \u03c0\u03b1\u03c1\u03ac\u03ba\u03b1\u03bc\u03c8\u03b7\u03c2" + }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c0\u03b1\u03c1\u03ac\u03ba\u03b1\u03bc\u03c8\u03b7 \u03c4\u03cd\u03c0\u03bf\u03c5 \"\u03a4\u03ce\u03c1\u03b1\" \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c4\u03b5\u03c1\u03bc\u03b1\u03c4\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c0\u03b1\u03c1\u03ac\u03ba\u03b1\u03bc\u03c8\u03b7 \u03c3\u03c4\u03b7\u03bd \u03b1\u03bb\u03bb\u03b1\u03b3\u03ae \u03c0\u03c1\u03bf\u03c6\u03af\u03bb \u03c4\u03b7\u03bd \u03b5\u03c0\u03cc\u03bc\u03b5\u03bd\u03b7 \u03b5\u03b2\u03b4\u03bf\u03bc\u03ac\u03b4\u03b1." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nobo_hub/translations/en.json b/homeassistant/components/nobo_hub/translations/en.json new file mode 100644 index 00000000000..b35a32101c3 --- /dev/null +++ b/homeassistant/components/nobo_hub/translations/en.json @@ -0,0 +1,44 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect - check serial number", + "invalid_ip": "Invalid IP address", + "invalid_serial": "Invalid serial number", + "unknown": "Unexpected error" + }, + "step": { + "manual": { + "data": { + "ip_address": "IP Address", + "serial": "Serial number (12 digits)" + }, + "description": "Configure a Nob\u00f8 Ecohub not discovered on your local network. If your hub is on another network, you can still connect to it by entering the complete serial number (12 digits) and its IP address." + }, + "selected": { + "data": { + "serial_suffix": "Serial number suffix (3 digits)" + }, + "description": "Configuring {hub}.\r\rTo connect to the hub, you need to enter the last 3 digits of the hub's serial number." + }, + "user": { + "data": { + "device": "Discovered hubs" + }, + "description": "Select Nob\u00f8 Ecohub to configure." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "override_type": "Override type" + }, + "description": "Select override type \"Now\" to end override on next week profile change." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nobo_hub/translations/es.json b/homeassistant/components/nobo_hub/translations/es.json new file mode 100644 index 00000000000..3985b698acd --- /dev/null +++ b/homeassistant/components/nobo_hub/translations/es.json @@ -0,0 +1,44 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "No se pudo conectar: comprueba el n\u00famero de serie", + "invalid_ip": "Direcci\u00f3n IP no v\u00e1lida", + "invalid_serial": "N\u00famero de serie no v\u00e1lido", + "unknown": "Error inesperado" + }, + "step": { + "manual": { + "data": { + "ip_address": "Direcci\u00f3n IP", + "serial": "N\u00famero de serie (12 d\u00edgitos)" + }, + "description": "Configura un Nob\u00f8 Ecohub no descubierto en tu red local. Si tu hub est\u00e1 en otra red, a\u00fan puedes conectarte introduciendo el n\u00famero de serie completo (12 d\u00edgitos) y su direcci\u00f3n IP." + }, + "selected": { + "data": { + "serial_suffix": "Sufijo del n\u00famero de serie (3 d\u00edgitos)" + }, + "description": "Configurando {hub}.\n\nPara conectarte al hub, debes introducir los 3 \u00faltimos d\u00edgitos del n\u00famero de serie del mismo." + }, + "user": { + "data": { + "device": "Hubs descubiertos" + }, + "description": "Selecciona un Nob\u00f8 Ecohub para configurar." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "override_type": "Tipo de anulaci\u00f3n" + }, + "description": "Selecciona el tipo de anulaci\u00f3n \"Ahora\" para finalizar la anulaci\u00f3n en el cambio de perfil de la pr\u00f3xima semana." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nobo_hub/translations/et.json b/homeassistant/components/nobo_hub/translations/et.json new file mode 100644 index 00000000000..bd1cf7c5bc0 --- /dev/null +++ b/homeassistant/components/nobo_hub/translations/et.json @@ -0,0 +1,44 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus - kontrolli seerianumbrit", + "invalid_ip": "Sobimatu IP-aadress", + "invalid_serial": "Sobimatu seerianumber", + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "manual": { + "data": { + "ip_address": "IP aadress", + "serial": "Seerianumber (12 numbrit)" + }, + "description": "Seadista Nob\u00f8 Ecohub, mida ei ole teie kohalikus v\u00f5rgus avastatud. Kui hub asub teises v\u00f5rgus, saad sellega ikkagi \u00fchendust luua, sisestades t\u00e4ieliku seerianumbri (12 numbrit) ja IP-aadressi." + }, + "selected": { + "data": { + "serial_suffix": "Seerianumbri j\u00e4relliide (3 numbrit)" + }, + "description": "{hub}-i seadistamine.\n\nJaoturiga \u00fchenduse loomiseks pead sisestama jaoturi seerianumbri viimased 3 numbrit." + }, + "user": { + "data": { + "device": "Avastatud s\u00f5lmpunktid" + }, + "description": "Vali konfigureeritav Nob\u00f8 Ecohub." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "override_type": "Alistamise t\u00fc\u00fcp" + }, + "description": "Vali alistamise t\u00fc\u00fcp \"N\u00fc\u00fcd\", et l\u00f5petada alistamine j\u00e4rgmisel n\u00e4dalal profiili muutmisel." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nobo_hub/translations/fr.json b/homeassistant/components/nobo_hub/translations/fr.json new file mode 100644 index 00000000000..76a7f9b25d9 --- /dev/null +++ b/homeassistant/components/nobo_hub/translations/fr.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "\u00c9chec de la connexion \u2013\u00a0v\u00e9rifiez le num\u00e9ro de s\u00e9rie", + "invalid_ip": "Adresse IP non valide", + "invalid_serial": "Num\u00e9ro de s\u00e9rie non valide", + "unknown": "Erreur inattendue" + }, + "step": { + "manual": { + "data": { + "ip_address": "Adresse IP", + "serial": "Num\u00e9ro de s\u00e9rie (12\u00a0chiffres)" + } + }, + "selected": { + "data": { + "serial_suffix": "Suffixe du num\u00e9ro de s\u00e9rie (3\u00a0chiffres)" + } + }, + "user": { + "data": { + "device": "Hubs d\u00e9couverts" + }, + "description": "S\u00e9lectionnez le Nob\u00f8 Ecohub \u00e0 configurer." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/he.json b/homeassistant/components/nobo_hub/translations/he.json similarity index 57% rename from homeassistant/components/flunearyou/translations/he.json rename to homeassistant/components/nobo_hub/translations/he.json index 02a79d5fbcc..93c0754ba16 100644 --- a/homeassistant/components/flunearyou/translations/he.json +++ b/homeassistant/components/nobo_hub/translations/he.json @@ -1,16 +1,15 @@ { "config": { "abort": { - "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05de\u05d9\u05e7\u05d5\u05dd \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" }, "error": { "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" }, "step": { - "user": { + "manual": { "data": { - "latitude": "\u05e7\u05d5 \u05e8\u05d5\u05d7\u05d1", - "longitude": "\u05e7\u05d5 \u05d0\u05d5\u05e8\u05da" + "ip_address": "\u05db\u05ea\u05d5\u05d1\u05ea IP" } } } diff --git a/homeassistant/components/nobo_hub/translations/hu.json b/homeassistant/components/nobo_hub/translations/hu.json new file mode 100644 index 00000000000..9058c96d3d6 --- /dev/null +++ b/homeassistant/components/nobo_hub/translations/hu.json @@ -0,0 +1,44 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "cannot_connect": "Nem siker\u00fclt csatlakozni - ellen\u0151rizze a sorozatsz\u00e1mot", + "invalid_ip": "\u00c9rv\u00e9nytelen IP-c\u00edm", + "invalid_serial": "\u00c9rv\u00e9nytelen sorozatsz\u00e1m", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "step": { + "manual": { + "data": { + "ip_address": "IP c\u00edm", + "serial": "Sorsz\u00e1m (12 sz\u00e1mjegy)" + }, + "description": "Konfigur\u00e1ljon egy Nob\u00f8 Ecohubot, amely nem tal\u00e1lhat\u00f3 a helyi h\u00e1l\u00f3zaton. Ha a hub egy m\u00e1sik h\u00e1l\u00f3zaton van, tov\u00e1bbra is csatlakozhat hozz\u00e1 a teljes sorozatsz\u00e1m (12 sz\u00e1mjegy) \u00e9s IP-c\u00edm\u00e9nek megad\u00e1s\u00e1val." + }, + "selected": { + "data": { + "serial_suffix": "Sorozatsz\u00e1m ut\u00f3tag (3 sz\u00e1mjegy)" + }, + "description": "{hub} konfigur\u00e1l\u00e1sa. A hubhoz val\u00f3 csatlakoz\u00e1shoz meg kell adnia a hub sorozatsz\u00e1m\u00e1nak utols\u00f3 3 sz\u00e1mjegy\u00e9t." + }, + "user": { + "data": { + "device": "Felfedezett hub-ok" + }, + "description": "V\u00e1lassza ki a Nob\u00f8 Ecohubot a konfigur\u00e1l\u00e1shoz." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "override_type": "T\u00edpus fel\u00fclb\u00edr\u00e1l\u00e1sa" + }, + "description": "V\u00e1lassza a \"Most\" fel\u00fclb\u00edr\u00e1l\u00e1si t\u00edpust a fel\u00fclb\u00edr\u00e1l\u00e1s megsz\u00fcntet\u00e9s\u00e9hez a k\u00f6vetkez\u0151 heti profilv\u00e1lt\u00e1skor." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nobo_hub/translations/id.json b/homeassistant/components/nobo_hub/translations/id.json new file mode 100644 index 00000000000..19a2f01ebc5 --- /dev/null +++ b/homeassistant/components/nobo_hub/translations/id.json @@ -0,0 +1,44 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung - periksa nomor seri", + "invalid_ip": "Alamat IP tidak valid", + "invalid_serial": "Nomor seri tidak valid", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "manual": { + "data": { + "ip_address": "Alamat IP", + "serial": "Nomor seri (12 digit)" + }, + "description": "Konfigurasikan Nob\u00f8 Ecohub yang tidak ditemukan di jaringan lokal Anda. Jika hub Anda berada di jaringan lain, Anda masih dapat menyambungkannya dengan memasukkan nomor seri lengkap (12 digit) dan alamat IP-nya." + }, + "selected": { + "data": { + "serial_suffix": "Akhiran nomor seri (3 digit)" + }, + "description": "Mengonfigurasi {hub}.\n\nUntuk terhubung ke hub, Anda harus memasukkan 3 digit terakhir dari nomor seri hub." + }, + "user": { + "data": { + "device": "Hub yang ditemukan" + }, + "description": "Pilih Nob\u00f8 Ecohub untuk dikonfigurasi." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "override_type": "Tipe penimpaan" + }, + "description": "Pilih tipe penimpaan \"Now\" untuk mengakhiri penimpaan pada perubahan profil minggu depan." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nobo_hub/translations/it.json b/homeassistant/components/nobo_hub/translations/it.json new file mode 100644 index 00000000000..2b8100c49e2 --- /dev/null +++ b/homeassistant/components/nobo_hub/translations/it.json @@ -0,0 +1,44 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi - controlla il numero di serie", + "invalid_ip": "Indirizzo IP non valido", + "invalid_serial": "Numero di serie non valido", + "unknown": "Errore imprevisto" + }, + "step": { + "manual": { + "data": { + "ip_address": "Indirizzo IP", + "serial": "Numero di serie (12 cifre)" + }, + "description": "Configura un Nob\u00f8 Ecohub non rilevato sulla tua rete locale. Se il tuo hub si trova su un'altra rete, puoi comunque connetterti inserendo il numero di serie completo (12 cifre) e il suo indirizzo IP." + }, + "selected": { + "data": { + "serial_suffix": "Suffisso del numero di serie (3 cifre)" + }, + "description": "Configurazione {hub}.\n\nPer connettersi all'hub, \u00e8 necessario inserire le ultime 3 cifre del numero di serie dell'hub." + }, + "user": { + "data": { + "device": "Hub scoperti" + }, + "description": "Seleziona Nob\u00f8 Ecohub da configurare." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "override_type": "Tipo di sostituzione" + }, + "description": "Seleziona il tipo di sostituzione \"Ora\" per terminare la sostituzione nella modifica del profilo della prossima settimana." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nobo_hub/translations/ja.json b/homeassistant/components/nobo_hub/translations/ja.json new file mode 100644 index 00000000000..3b99fe98872 --- /dev/null +++ b/homeassistant/components/nobo_hub/translations/ja.json @@ -0,0 +1,44 @@ +{ + "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f - \u30b7\u30ea\u30a2\u30eb\u756a\u53f7\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044", + "invalid_ip": "\u7121\u52b9\u306aIP\u30a2\u30c9\u30ec\u30b9", + "invalid_serial": "\u7121\u52b9\u306a\u30b7\u30ea\u30a2\u30eb\u756a\u53f7", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, + "step": { + "manual": { + "data": { + "ip_address": "IP\u30a2\u30c9\u30ec\u30b9", + "serial": "\u30b7\u30ea\u30a2\u30eb\u30ca\u30f3\u30d0\u30fc(12\u6841)" + }, + "description": "\u30ed\u30fc\u30ab\u30eb \u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u3067\u691c\u51fa\u3055\u308c\u306a\u3044 Nob\u00f8 Ecohub \u3092\u69cb\u6210\u3057\u307e\u3059\u3002\u30cf\u30d6\u304c\u5225\u306e\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u3042\u308b\u5834\u5408\u3067\u3082\u3001\u5b8c\u5168\u306a\u30b7\u30ea\u30a2\u30eb\u756a\u53f7 (12 \u6841) \u3068\u305d\u306e IP \u30a2\u30c9\u30ec\u30b9\u3092\u5165\u529b\u3059\u308b\u3053\u3068\u3067\u63a5\u7d9a\u3067\u304d\u307e\u3059\u3002" + }, + "selected": { + "data": { + "serial_suffix": "\u30b7\u30ea\u30a2\u30eb\u30ca\u30f3\u30d0\u30fc\u306e\u672b\u5c3e(3\u6841)" + }, + "description": "{hub} \u306e\u8a2d\u5b9a\u3002\n\n\u30cf\u30d6\u306b\u63a5\u7d9a\u3059\u308b\u306b\u306f\u3001\u30cf\u30d6\u306e\u30b7\u30ea\u30a2\u30eb\u756a\u53f7\u306e\u672b\u5c3e3\u6841\u3092\u5165\u529b\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002" + }, + "user": { + "data": { + "device": "\u691c\u51fa\u3055\u308c\u305f\u30cf\u30d6" + }, + "description": "Nob\u00f8 Ecohub\u3092\u9078\u629e\u3057\u3066\u8a2d\u5b9a\u3057\u307e\u3059\u3002" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "override_type": "\u30aa\u30fc\u30d0\u30fc\u30e9\u30a4\u30c9\u306e\u7a2e\u985e" + }, + "description": "\u30aa\u30fc\u30d0\u30fc\u30e9\u30a4\u30c9\u306e\u7a2e\u985e\u3092\"\u4eca\u3059\u3050\"\u306b\u3059\u308b\u3068\u3001\u6765\u9031\u306e\u30d7\u30ed\u30d5\u30a1\u30a4\u30eb\u5909\u66f4\u6642\u306b\u30aa\u30fc\u30d0\u30fc\u30e9\u30a4\u30c9\u304c\u7d42\u4e86\u3057\u307e\u3059\u3002" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nobo_hub/translations/nl.json b/homeassistant/components/nobo_hub/translations/nl.json new file mode 100644 index 00000000000..9a8e90bcdb5 --- /dev/null +++ b/homeassistant/components/nobo_hub/translations/nl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd" + }, + "error": { + "cannot_connect": "Kan niet verbinden. Controleer het serienummer.", + "invalid_ip": "Ongeldig IP-adres", + "invalid_serial": "Ongeldig serienummer", + "unknown": "Onverwachte fout" + }, + "step": { + "manual": { + "data": { + "ip_address": "IP-adres", + "serial": "Serienummer (12 cijfers)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nobo_hub/translations/no.json b/homeassistant/components/nobo_hub/translations/no.json new file mode 100644 index 00000000000..ffd37f30dee --- /dev/null +++ b/homeassistant/components/nobo_hub/translations/no.json @@ -0,0 +1,44 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "cannot_connect": "Tilkobling feilet - sjekk serienummer", + "invalid_ip": "Ugyldig IP-adresse", + "invalid_serial": "Ugyldig serienummer", + "unknown": "Uventet feil" + }, + "step": { + "manual": { + "data": { + "ip_address": "IP-adresse", + "serial": "Serienummer (12 sifre)" + }, + "description": "Konfigurer en Nob\u00f8 Ecohub som ikke er oppdaget p\u00e5 ditt lokale nettverk. Hvis huben er p\u00e5 et annet nettverk, kan du fortsatt koble den til med \u00e5 skrive inn fullstendig serienummer (12 sifre) og IP-adressen." + }, + "selected": { + "data": { + "serial_suffix": "Serienummersuffiks (3 sifre)" + }, + "description": "Konfigurerer {hub}.\n\nFor \u00e5 koble til huben, m\u00e5 du skrive inn de 3 siste sifrene i hubens serienummer." + }, + "user": { + "data": { + "device": "Oppdagede huber" + }, + "description": "Velg Nob\u00f8 Ecohub du vil konfigurere" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "override_type": "Overstyringstype" + }, + "description": "Velg overstyringstype \"Now\" for \u00e5 avslutte overstyringer ved neste endring i ukesprofilen." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nobo_hub/translations/pl.json b/homeassistant/components/nobo_hub/translations/pl.json new file mode 100644 index 00000000000..58964ecdede --- /dev/null +++ b/homeassistant/components/nobo_hub/translations/pl.json @@ -0,0 +1,44 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia \u2014 sprawd\u017a numer seryjny", + "invalid_ip": "Nieprawid\u0142owy adres IP", + "invalid_serial": "Nieprawid\u0142owy numer seryjny", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "manual": { + "data": { + "ip_address": "Adres IP", + "serial": "Numer seryjny (12 cyfr)" + }, + "description": "Skonfiguruj Nob\u00f8 Ecohub, kt\u00f3ry nie zosta\u0142 wykryty w Twojej sieci lokalnej. Je\u015bli koncentrator znajduje si\u0119 w innej sieci, nadal mo\u017cesz si\u0119 z nim po\u0142\u0105czy\u0107, wprowadzaj\u0105c pe\u0142ny numer seryjny (12 cyfr) i jego adres IP." + }, + "selected": { + "data": { + "serial_suffix": "Sufiks numeru seryjnego (3 cyfry)" + }, + "description": "Konfiguracja {hub}.\n\nAby po\u0142\u0105czy\u0107 si\u0119 z koncentratorem, musisz wprowadzi\u0107 3 ostatnie cyfry numeru seryjnego koncentratora." + }, + "user": { + "data": { + "device": "Wykryte huby" + }, + "description": "Wybierz Nob\u00f8 Ecohub do skonfigurowania." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "override_type": "Typ nadpisania" + }, + "description": "Wybierz typ nadpisania \u201eTeraz\u201d, aby zako\u0144czy\u0107 nadpisywania przy zmianie profilu w nast\u0119pnym tygodniu." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nobo_hub/translations/pt-BR.json b/homeassistant/components/nobo_hub/translations/pt-BR.json new file mode 100644 index 00000000000..48278bfe1cb --- /dev/null +++ b/homeassistant/components/nobo_hub/translations/pt-BR.json @@ -0,0 +1,44 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha ao conectar - verifique o n\u00famero de s\u00e9rie", + "invalid_ip": "Endere\u00e7o IP inv\u00e1lido", + "invalid_serial": "N\u00famero de s\u00e9rie inv\u00e1lido", + "unknown": "Erro inesperado" + }, + "step": { + "manual": { + "data": { + "ip_address": "Endere\u00e7o IP", + "serial": "N\u00famero de s\u00e9rie (12 d\u00edgitos)" + }, + "description": "Configure um Nob\u00f8 Ecohub n\u00e3o descoberto em sua rede local. Se o seu hub estiver em outra rede, voc\u00ea ainda poder\u00e1 se conectar a ele digitando o n\u00famero de s\u00e9rie completo (12 d\u00edgitos) e seu endere\u00e7o IP." + }, + "selected": { + "data": { + "serial_suffix": "Sufixo do n\u00famero de s\u00e9rie (3 d\u00edgitos)" + }, + "description": "Configurando {hub}. Para se conectar ao hub, voc\u00ea precisa inserir os 3 \u00faltimos d\u00edgitos do n\u00famero de s\u00e9rie do hub." + }, + "user": { + "data": { + "device": "Hubs descobertos" + }, + "description": "Selecione Nob\u00f8 Ecohub para configurar." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "override_type": "Tipo de substitui\u00e7\u00e3o" + }, + "description": "Selecione o tipo de substitui\u00e7\u00e3o \"Agora\" para encerrar a substitui\u00e7\u00e3o na pr\u00f3xima semana de altera\u00e7\u00e3o de perfil." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nobo_hub/translations/pt.json b/homeassistant/components/nobo_hub/translations/pt.json new file mode 100644 index 00000000000..0749b1fe831 --- /dev/null +++ b/homeassistant/components/nobo_hub/translations/pt.json @@ -0,0 +1,39 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha ao conectar - verifique o n\u00famero de s\u00e9rie", + "invalid_ip": "Endere\u00e7o IP inv\u00e1lido", + "invalid_serial": "N\u00famero de s\u00e9rie inv\u00e1lido" + }, + "step": { + "manual": { + "data": { + "serial": "N\u00famero de s\u00e9rie (12 d\u00edgitos)" + }, + "description": "Configure um Nob\u00f8 Ecohub n\u00e3o descoberto em sua rede local. Se o seu hub estiver em outra rede, voc\u00ea ainda poder\u00e1 se conectar a ele digitando o n\u00famero de s\u00e9rie completo (12 d\u00edgitos) e seu endere\u00e7o IP." + }, + "selected": { + "data": { + "serial_suffix": "Sufixo do n\u00famero de s\u00e9rie (3 d\u00edgitos)" + }, + "description": "Configurando {hub} . Para se conectar ao hub, voc\u00ea precisa inserir os 3 \u00faltimos d\u00edgitos do n\u00famero de s\u00e9rie do hub." + }, + "user": { + "data": { + "device": "Hubs descobertos" + }, + "description": "Selecione Nob\u00f8 Ecohub para configurar." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "override_type": "Tipo de substitui\u00e7\u00e3o" + }, + "description": "Selecione o tipo de substitui\u00e7\u00e3o \"Agora\" para encerrar a substitui\u00e7\u00e3o na pr\u00f3xima semana de altera\u00e7\u00e3o de perfil." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nobo_hub/translations/ru.json b/homeassistant/components/nobo_hub/translations/ru.json new file mode 100644 index 00000000000..e5932ac1e03 --- /dev/null +++ b/homeassistant/components/nobo_hub/translations/ru.json @@ -0,0 +1,44 @@ +{ + "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\u0432\u0435\u0440\u044c\u0442\u0435 \u0441\u0435\u0440\u0438\u0439\u043d\u044b\u0439 \u043d\u043e\u043c\u0435\u0440.", + "invalid_ip": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 IP-\u0430\u0434\u0440\u0435\u0441.", + "invalid_serial": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0441\u0435\u0440\u0438\u0439\u043d\u044b\u0439 \u043d\u043e\u043c\u0435\u0440.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "manual": { + "data": { + "ip_address": "IP-\u0430\u0434\u0440\u0435\u0441", + "serial": "\u0421\u0435\u0440\u0438\u0439\u043d\u044b\u0439 \u043d\u043e\u043c\u0435\u0440 (12 \u0446\u0438\u0444\u0440)" + }, + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Nob\u00f8 Ecohub, \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043d\u043e\u0433\u043e \u0432 \u0412\u0430\u0448\u0435\u0439 \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u043e\u0439 \u0441\u0435\u0442\u0438. \u0415\u0441\u043b\u0438 \u0412\u0430\u0448 \u0431\u043b\u043e\u043a \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f \u043d\u0430\u0445\u043e\u0434\u0438\u0442\u0441\u044f \u0432 \u0434\u0440\u0443\u0433\u043e\u0439 \u0441\u0435\u0442\u0438, \u0412\u044b \u0432\u0441\u0435 \u0440\u0430\u0432\u043d\u043e \u043c\u043e\u0436\u0435\u0442\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u043d\u0435\u043c\u0443, \u0432\u0432\u0435\u0434\u044f \u043f\u043e\u043b\u043d\u044b\u0439 \u0441\u0435\u0440\u0438\u0439\u043d\u044b\u0439 \u043d\u043e\u043c\u0435\u0440 (12 \u0446\u0438\u0444\u0440) \u0438 \u0435\u0433\u043e IP-\u0430\u0434\u0440\u0435\u0441." + }, + "selected": { + "data": { + "serial_suffix": "\u0421\u0443\u0444\u0444\u0438\u043a\u0441 \u0441\u0435\u0440\u0438\u0439\u043d\u043e\u0433\u043e \u043d\u043e\u043c\u0435\u0440\u0430 (3 \u0446\u0438\u0444\u0440\u044b)" + }, + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 {hub}. \u0414\u043b\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0432\u0432\u0435\u0441\u0442\u0438 \u043f\u043e\u0441\u043b\u0435\u0434\u043d\u0438\u0435 3 \u0446\u0438\u0444\u0440\u044b \u0441\u0435\u0440\u0438\u0439\u043d\u043e\u0433\u043e \u043d\u043e\u043c\u0435\u0440\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430." + }, + "user": { + "data": { + "device": "\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043d\u044b\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 Nob\u00f8 Ecohub \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "override_type": "\u0422\u0438\u043f \u043f\u0435\u0440\u0435\u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u0438\u044f" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0442\u0438\u043f \u043f\u0435\u0440\u0435\u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u0438\u044f \u00abNow\u00bb, \u0447\u0442\u043e\u0431\u044b \u043e\u0442\u043c\u0435\u043d\u0438\u0442\u044c \u043f\u0435\u0440\u0435\u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u0438\u0435 \u043f\u0440\u0438 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0438 \u043f\u0440\u043e\u0444\u0438\u043b\u044f \u043d\u0430 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0435\u0439 \u043d\u0435\u0434\u0435\u043b\u0435." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nobo_hub/translations/sv.json b/homeassistant/components/nobo_hub/translations/sv.json new file mode 100644 index 00000000000..c4306e930d2 --- /dev/null +++ b/homeassistant/components/nobo_hub/translations/sv.json @@ -0,0 +1,44 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta - kontrollera serienumret", + "invalid_ip": "Ogiltig IP-adress", + "invalid_serial": "Ogiltigt serienummer", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "manual": { + "data": { + "ip_address": "IP-adress", + "serial": "Serienummer (12 siffror)" + }, + "description": "Konfigurera en Nob\u00f8 Ecohub som inte uppt\u00e4ckts i ditt lokala n\u00e4tverk. Om din hubb \u00e4r p\u00e5 ett annat n\u00e4tverk kan du fortfarande ansluta till det genom att ange hela serienumret (12 siffror) och dess IP-adress." + }, + "selected": { + "data": { + "serial_suffix": "Serienummersuffix (3 siffror)" + }, + "description": "Konfigurerar {hub} . F\u00f6r att ansluta till hubben m\u00e5ste du ange de tre sista siffrorna i hubbens serienummer." + }, + "user": { + "data": { + "device": "Identifierade hubbar" + }, + "description": "V\u00e4lj Nob\u00f8 Ecohub f\u00f6r att konfigurera." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "override_type": "\u00c5sidos\u00e4tt typ" + }, + "description": "V\u00e4lj \u00e5sidos\u00e4ttningstyp \"Nu\" f\u00f6r att avsluta \u00e5sidos\u00e4ttningen vid n\u00e4sta veckas profil\u00e4ndring." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nobo_hub/translations/tr.json b/homeassistant/components/nobo_hub/translations/tr.json new file mode 100644 index 00000000000..8dda9b88c76 --- /dev/null +++ b/homeassistant/components/nobo_hub/translations/tr.json @@ -0,0 +1,44 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flant\u0131 kurulamad\u0131 - seri numaras\u0131n\u0131 kontrol edin", + "invalid_ip": "Ge\u00e7ersiz IP adresi", + "invalid_serial": "Ge\u00e7ersiz seri numaras\u0131", + "unknown": "Beklenmeyen hata" + }, + "step": { + "manual": { + "data": { + "ip_address": "IP Adresi", + "serial": "Seri numaras\u0131 (12 haneli)" + }, + "description": "Yerel a\u011f\u0131n\u0131zda ke\u015ffedilmemi\u015f bir Nob\u00f8 Ecohub yap\u0131land\u0131r\u0131n. Hub'\u0131n\u0131z ba\u015fka bir a\u011fdaysa, tam seri numaras\u0131n\u0131 (12 haneli) ve IP adresini girerek yine de hub'a ba\u011flanabilirsiniz." + }, + "selected": { + "data": { + "serial_suffix": "Seri numaras\u0131 son eki (3 basamak)" + }, + "description": "{hub} yap\u0131land\u0131r\u0131l\u0131yor. Hub'a ba\u011flanmak i\u00e7in hub'\u0131n seri numaras\u0131n\u0131n son 3 hanesini girmeniz gerekir." + }, + "user": { + "data": { + "device": "Ke\u015ffedilen hub'lar" + }, + "description": "Yap\u0131land\u0131rmak i\u00e7in Nob\u00f8 Ecohub \u00f6\u011fesini se\u00e7in." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "override_type": "Ge\u00e7ersiz k\u0131lma t\u00fcr\u00fc" + }, + "description": "Sonraki hafta profil de\u011fi\u015fikli\u011finde ge\u00e7ersiz k\u0131lmay\u0131 sonland\u0131rmak i\u00e7in \"\u015eimdi\" ge\u00e7ersiz k\u0131lma t\u00fcr\u00fcn\u00fc se\u00e7in." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nobo_hub/translations/zh-Hant.json b/homeassistant/components/nobo_hub/translations/zh-Hant.json new file mode 100644 index 00000000000..1d2f4ccf917 --- /dev/null +++ b/homeassistant/components/nobo_hub/translations/zh-Hant.json @@ -0,0 +1,44 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557 - \u8acb\u6aa2\u67e5\u5e8f\u865f", + "invalid_ip": "IP \u4f4d\u5740\u7121\u6548", + "invalid_serial": "\u5e8f\u865f\u7121\u6548", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "manual": { + "data": { + "ip_address": "IP \u4f4d\u5740", + "serial": "\u5e8f\u865f\uff0812 \u4f4d\uff09" + }, + "description": "\u8a2d\u5b9a\u672c\u5730\u7db2\u8def\u672a\u81ea\u52d5\u767c\u73fe\u7684 Nob\u00f8 Ecohub\u3002 \u5047\u5982 hub \u9023\u7dda\u81f3\u5176\u4ed6\u7db2\u8def\u3001\u53ef\u4ee5\u8a66\u8457\u8f38\u5165\u5b8c\u6574\u7684\u5e8f\u865f\uff0812 \u4f4d\u6578\uff09\u53ca\u5176 IP \u4f4d\u5740\u9032\u884c\u9023\u7dda\u3002" + }, + "selected": { + "data": { + "serial_suffix": "\u5e8f\u865f\u5f8c\u7db4\uff083 \u4f4d\u6578\uff09" + }, + "description": "\u8a2d\u5b9a {hub}\u3002\n\n\u6b32\u9023\u7dda\u81f3 Hub\u3001\u9700\u8981\u8f38\u5165\u81f3\u5c11 3 \u4f4d\u6578\u4e4b Hub \u5e8f\u865f\u3002" + }, + "user": { + "data": { + "device": "\u81ea\u52d5\u641c\u7d22\u5230\u7684 Hub" + }, + "description": "\u9078\u64c7 Nob\u00f8 Ecohub \u4ee5\u9032\u884c\u8a2d\u5b9a\u3002" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "override_type": "\u8986\u5beb\u985e\u5225" + }, + "description": "\u9078\u64c7\u8986\u5beb\u985e\u5225 \"Now\" \u4ee5\u65bc\u4e0b\u9031\u6a94\u6848\u8b8a\u66f4\u6642\u7d50\u675f\u8986\u5beb\u3002" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/norway_air/air_quality.py b/homeassistant/components/norway_air/air_quality.py index b6182d7ed84..b4acdc3bdc9 100644 --- a/homeassistant/components/norway_air/air_quality.py +++ b/homeassistant/components/norway_air/air_quality.py @@ -106,31 +106,31 @@ class AirSensor(AirQualityEntity): """Return the name of the sensor.""" return self._name - @property # type: ignore[misc] + @property @round_state def air_quality_index(self): """Return the Air Quality Index (AQI).""" return self._api.data.get("aqi") - @property # type: ignore[misc] + @property @round_state def nitrogen_dioxide(self): """Return the NO2 (nitrogen dioxide) level.""" return self._api.data.get("no2_concentration") - @property # type: ignore[misc] + @property @round_state def ozone(self): """Return the O3 (ozone) level.""" return self._api.data.get("o3_concentration") - @property # type: ignore[misc] + @property @round_state def particulate_matter_2_5(self): """Return the particulate matter 2.5 level.""" return self._api.data.get("pm25_concentration") - @property # type: ignore[misc] + @property @round_state def particulate_matter_10(self): """Return the particulate matter 10 level.""" diff --git a/homeassistant/components/notify/__init__.py b/homeassistant/components/notify/__init__.py index 60d24578593..52864dd001d 100644 --- a/homeassistant/components/notify/__init__.py +++ b/homeassistant/components/notify/__init__.py @@ -57,7 +57,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: await asyncio.wait([asyncio.create_task(setup) for setup in platform_setups]) async def persistent_notification(service: ServiceCall) -> None: - """Send notification via the built-in persistsent_notify integration.""" + """Send notification via the built-in persistent_notify integration.""" message = service.data[ATTR_MESSAGE] message.hass = hass check_templates_warn(hass, message) diff --git a/homeassistant/components/notify/legacy.py b/homeassistant/components/notify/legacy.py index f9066b7dff9..c3bb02896e0 100644 --- a/homeassistant/components/notify/legacy.py +++ b/homeassistant/components/notify/legacy.py @@ -2,9 +2,9 @@ from __future__ import annotations import asyncio -from collections.abc import Coroutine +from collections.abc import Callable, Coroutine from functools import partial -from typing import Any, cast +from typing import Any, Optional, Protocol, cast from homeassistant.const import CONF_DESCRIPTION, CONF_NAME from homeassistant.core import HomeAssistant, ServiceCall, callback @@ -33,6 +33,26 @@ NOTIFY_SERVICES = "notify_services" NOTIFY_DISCOVERY_DISPATCHER = "notify_discovery_dispatcher" +class LegacyNotifyPlatform(Protocol): + """Define the format of legacy notify platforms.""" + + async def async_get_service( + self, + hass: HomeAssistant, + config: ConfigType, + discovery_info: DiscoveryInfoType | None = ..., + ) -> BaseNotificationService: + """Set up notification service.""" + + def get_service( + self, + hass: HomeAssistant, + config: ConfigType, + discovery_info: DiscoveryInfoType | None = ..., + ) -> BaseNotificationService: + """Set up notification service.""" + + @callback def async_setup_legacy( hass: HomeAssistant, config: ConfigType @@ -50,8 +70,9 @@ def async_setup_legacy( if p_config is None: p_config = {} - platform = await async_prepare_setup_platform( - hass, config, DOMAIN, integration_name + platform = cast( + Optional[LegacyNotifyPlatform], + await async_prepare_setup_platform(hass, config, DOMAIN, integration_name), ) if platform is None: @@ -141,9 +162,11 @@ async def async_reload(hass: HomeAssistant, integration_name: str) -> None: if not _async_integration_has_notify_services(hass, integration_name): return + notify_services: list[BaseNotificationService] = hass.data[NOTIFY_SERVICES][ + integration_name + ] tasks = [ - notify_service.async_register_services() - for notify_service in hass.data[NOTIFY_SERVICES][integration_name] + notify_service.async_register_services() for notify_service in notify_services ] await asyncio.gather(*tasks) @@ -152,15 +175,20 @@ async def async_reload(hass: HomeAssistant, integration_name: str) -> None: @bind_hass async def async_reset_platform(hass: HomeAssistant, integration_name: str) -> None: """Unregister notify services for an integration.""" - if NOTIFY_DISCOVERY_DISPATCHER in hass.data: - hass.data[NOTIFY_DISCOVERY_DISPATCHER]() + notify_discovery_dispatcher: Callable[[], None] | None = hass.data.get( + NOTIFY_DISCOVERY_DISPATCHER + ) + if notify_discovery_dispatcher: + notify_discovery_dispatcher() hass.data[NOTIFY_DISCOVERY_DISPATCHER] = None if not _async_integration_has_notify_services(hass, integration_name): return + notify_services: list[BaseNotificationService] = hass.data[NOTIFY_SERVICES][ + integration_name + ] tasks = [ - notify_service.async_unregister_services() - for notify_service in hass.data[NOTIFY_SERVICES][integration_name] + notify_service.async_unregister_services() for notify_service in notify_services ] await asyncio.gather(*tasks) diff --git a/homeassistant/components/notify/manifest.json b/homeassistant/components/notify/manifest.json index b32295a10a6..3e930b32ba6 100644 --- a/homeassistant/components/notify/manifest.json +++ b/homeassistant/components/notify/manifest.json @@ -3,5 +3,6 @@ "name": "Notifications", "documentation": "https://www.home-assistant.io/integrations/notify", "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "entity" } diff --git a/homeassistant/components/notion/translations/es.json b/homeassistant/components/notion/translations/es.json index f7a7a01f4d8..9c649324dc6 100644 --- a/homeassistant/components/notion/translations/es.json +++ b/homeassistant/components/notion/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "error": { "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", diff --git a/homeassistant/components/nuheat/climate.py b/homeassistant/components/nuheat/climate.py index 6cc70965ade..c731e3472d6 100644 --- a/homeassistant/components/nuheat/climate.py +++ b/homeassistant/components/nuheat/climate.py @@ -2,6 +2,7 @@ from datetime import datetime import logging import time +from typing import Any from nuheat.config import SCHEDULE_HOLD, SCHEDULE_RUN, SCHEDULE_TEMPORARY_HOLD from nuheat.util import ( @@ -11,9 +12,9 @@ from nuheat.util import ( nuheat_to_fahrenheit, ) -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( ATTR_HVAC_MODE, + ClimateEntity, ClimateEntityFeature, HVACAction, HVACMode, @@ -100,7 +101,7 @@ class NuHeatThermostat(CoordinatorEntity, ClimateEntity): return self._thermostat.room @property - def temperature_unit(self): + def temperature_unit(self) -> str: """Return the unit of measurement.""" if self._temperature_unit == "C": return TEMP_CELSIUS @@ -121,7 +122,7 @@ class NuHeatThermostat(CoordinatorEntity, ClimateEntity): return self._thermostat.serial_number @property - def available(self): + def available(self) -> bool: """Return the unique id.""" return self.coordinator.last_update_success and self._thermostat.online @@ -178,7 +179,7 @@ class NuHeatThermostat(CoordinatorEntity, ClimateEntity): """Return available preset modes.""" return PRESET_MODES - def set_preset_mode(self, preset_mode): + def set_preset_mode(self, preset_mode: str) -> None: """Update the hold mode of the thermostat.""" self._set_schedule_mode( PRESET_MODE_TO_SCHEDULE_MODE_MAP.get(preset_mode, SCHEDULE_RUN) @@ -191,7 +192,7 @@ class NuHeatThermostat(CoordinatorEntity, ClimateEntity): self._thermostat.schedule_mode = schedule_mode self._schedule_update() - def set_temperature(self, **kwargs): + def set_temperature(self, **kwargs: Any) -> None: """Set a new target temperature.""" self._set_temperature_and_mode( kwargs.get(ATTR_TEMPERATURE), hvac_mode=kwargs.get(ATTR_HVAC_MODE) diff --git a/homeassistant/components/nuki/binary_sensor.py b/homeassistant/components/nuki/binary_sensor.py index 4c59c121e6d..69c48133533 100644 --- a/homeassistant/components/nuki/binary_sensor.py +++ b/homeassistant/components/nuki/binary_sensor.py @@ -54,7 +54,7 @@ class NukiDoorsensorEntity(NukiEntity, BinarySensorEntity): return data @property - def available(self): + def available(self) -> bool: """Return true if door sensor is present and activated.""" return super().available and self._nuki_device.is_door_sensor_activated diff --git a/homeassistant/components/nuki/translations/es.json b/homeassistant/components/nuki/translations/es.json index d21ef9dfdb6..53ef4b360df 100644 --- a/homeassistant/components/nuki/translations/es.json +++ b/homeassistant/components/nuki/translations/es.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "error": { "cannot_connect": "No se pudo conectar", diff --git a/homeassistant/components/numato/binary_sensor.py b/homeassistant/components/numato/binary_sensor.py index b3881cc0493..c326a45d462 100644 --- a/homeassistant/components/numato/binary_sensor.py +++ b/homeassistant/components/numato/binary_sensor.py @@ -92,7 +92,7 @@ class NumatoGpioBinarySensor(BinarySensorEntity): self._state = None self._api = api - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Connect state update callback.""" self.async_on_remove( async_dispatcher_connect( @@ -118,7 +118,7 @@ class NumatoGpioBinarySensor(BinarySensorEntity): """Return the state of the entity.""" return self._state != self._invert_logic - def update(self): + def update(self) -> None: """Update the GPIO state.""" try: self._state = self._api.read_input(self._device_id, self._port) diff --git a/homeassistant/components/numato/sensor.py b/homeassistant/components/numato/sensor.py index 8183e4c6796..4ac28e07611 100644 --- a/homeassistant/components/numato/sensor.py +++ b/homeassistant/components/numato/sensor.py @@ -102,7 +102,7 @@ class NumatoGpioAdc(SensorEntity): """Return the icon to use in the frontend, if any.""" return ICON - def update(self): + def update(self) -> None: """Get the latest data and updates the state.""" try: adc_val = self._api.read_adc_input(self._device_id, self._port) diff --git a/homeassistant/components/numato/switch.py b/homeassistant/components/numato/switch.py index fb18866ae93..92fc7e0e2df 100644 --- a/homeassistant/components/numato/switch.py +++ b/homeassistant/components/numato/switch.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any from numato_gpio import NumatoGpioError @@ -88,7 +89,7 @@ class NumatoGpioSwitch(SwitchEntity): """Return true if port is turned on.""" return self._state - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the port on.""" try: self._api.write_output( @@ -104,7 +105,7 @@ class NumatoGpioSwitch(SwitchEntity): err, ) - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the port off.""" try: self._api.write_output( diff --git a/homeassistant/components/number/__init__.py b/homeassistant/components/number/__init__.py index 4990fbfa7f8..0012a4b77ff 100644 --- a/homeassistant/components/number/__init__.py +++ b/homeassistant/components/number/__init__.py @@ -29,7 +29,7 @@ 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 import temperature as temperature_util +from homeassistant.util.unit_conversion import BaseUnitConverter, TemperatureConverter from .const import ( ATTR_MAX, @@ -70,18 +70,16 @@ class NumberMode(StrEnum): SLIDER = "slider" -UNIT_CONVERSIONS: dict[str, Callable[[float, str, str], float]] = { - NumberDeviceClass.TEMPERATURE: temperature_util.convert, +UNIT_CONVERTERS: dict[str, type[BaseUnitConverter]] = { + NumberDeviceClass.TEMPERATURE: TemperatureConverter, } -VALID_UNITS: dict[str, tuple[str, ...]] = { - NumberDeviceClass.TEMPERATURE: temperature_util.VALID_UNITS, -} +# mypy: disallow-any-generics async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up Number entities.""" - component = hass.data[DOMAIN] = EntityComponent( + component = hass.data[DOMAIN] = EntityComponent[NumberEntity]( _LOGGER, DOMAIN, hass, SCAN_INTERVAL ) await component.async_setup(config) @@ -115,13 +113,13 @@ async def async_set_value(entity: NumberEntity, service_call: ServiceCall) -> No async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry.""" - component: EntityComponent = hass.data[DOMAIN] + component: EntityComponent[NumberEntity] = hass.data[DOMAIN] return await component.async_setup_entry(entry) async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - component: EntityComponent = hass.data[DOMAIN] + component: EntityComponent[NumberEntity] = hass.data[DOMAIN] return await component.async_unload_entry(entry) @@ -423,7 +421,9 @@ class NumberEntity(Entity): """Set new value.""" await self.hass.async_add_executor_job(self.set_value, value) - def _convert_to_state_value(self, value: float, method: Callable) -> float: + def _convert_to_state_value( + self, value: float, method: Callable[[float, int], float] + ) -> float: """Convert a value in the number's native unit to the configured unit.""" native_unit_of_measurement = self.native_unit_of_measurement @@ -432,7 +432,7 @@ class NumberEntity(Entity): if ( native_unit_of_measurement != unit_of_measurement - and device_class in UNIT_CONVERSIONS + and device_class in UNIT_CONVERTERS ): assert native_unit_of_measurement assert unit_of_measurement @@ -442,7 +442,7 @@ class NumberEntity(Entity): # Suppress ValueError (Could not convert value to float) with suppress(ValueError): - value_new: float = UNIT_CONVERSIONS[device_class]( + value_new: float = UNIT_CONVERTERS[device_class].convert( value, native_unit_of_measurement, unit_of_measurement, @@ -463,12 +463,12 @@ class NumberEntity(Entity): if ( value is not None and native_unit_of_measurement != unit_of_measurement - and device_class in UNIT_CONVERSIONS + and device_class in UNIT_CONVERTERS ): assert native_unit_of_measurement assert unit_of_measurement - value = UNIT_CONVERSIONS[device_class]( + value = UNIT_CONVERTERS[device_class].convert( value, unit_of_measurement, native_unit_of_measurement, @@ -496,9 +496,10 @@ class NumberEntity(Entity): if ( (number_options := self.registry_entry.options.get(DOMAIN)) and (custom_unit := number_options.get(CONF_UNIT_OF_MEASUREMENT)) - and (device_class := self.device_class) in UNIT_CONVERSIONS - and self.native_unit_of_measurement in VALID_UNITS[device_class] - and custom_unit in VALID_UNITS[device_class] + 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 ): self._number_option_unit_of_measurement = custom_unit return diff --git a/homeassistant/components/number/manifest.json b/homeassistant/components/number/manifest.json index 549494fa3f5..4cb16c8e0c3 100644 --- a/homeassistant/components/number/manifest.json +++ b/homeassistant/components/number/manifest.json @@ -3,5 +3,6 @@ "name": "Number", "documentation": "https://www.home-assistant.io/integrations/number", "codeowners": ["@home-assistant/core", "@Shulyaka"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "entity" } diff --git a/homeassistant/components/nws/sensor.py b/homeassistant/components/nws/sensor.py index 3bcfd19407d..1e062bf3d36 100644 --- a/homeassistant/components/nws/sensor.py +++ b/homeassistant/components/nws/sensor.py @@ -17,10 +17,12 @@ 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.util.distance import convert as convert_distance from homeassistant.util.dt import utcnow -from homeassistant.util.pressure import convert as convert_pressure -from homeassistant.util.speed import convert as convert_speed +from homeassistant.util.unit_conversion import ( + DistanceConverter, + PressureConverter, + SpeedConverter, +) from . import base_unique_id, device_info from .const import ( @@ -91,12 +93,16 @@ class NWSSensor(CoordinatorEntity, SensorEntity): unit_of_measurement = self.native_unit_of_measurement if unit_of_measurement == SPEED_MILES_PER_HOUR: return round( - convert_speed(value, SPEED_KILOMETERS_PER_HOUR, SPEED_MILES_PER_HOUR) + SpeedConverter.convert( + value, SPEED_KILOMETERS_PER_HOUR, SPEED_MILES_PER_HOUR + ) ) if unit_of_measurement == LENGTH_MILES: - return round(convert_distance(value, LENGTH_METERS, LENGTH_MILES)) + return round(DistanceConverter.convert(value, LENGTH_METERS, LENGTH_MILES)) if unit_of_measurement == PRESSURE_INHG: - return round(convert_pressure(value, PRESSURE_PA, PRESSURE_INHG), 2) + return round( + PressureConverter.convert(value, PRESSURE_PA, PRESSURE_INHG), 2 + ) if unit_of_measurement == TEMP_CELSIUS: return round(value, 1) if unit_of_measurement == PERCENTAGE: @@ -109,7 +115,7 @@ class NWSSensor(CoordinatorEntity, SensorEntity): return f"{base_unique_id(self._latitude, self._longitude)}_{self.entity_description.key}" @property - def available(self): + def available(self) -> bool: """Return if state is available.""" if self.coordinator.last_update_success_time: last_success_time = ( diff --git a/homeassistant/components/nws/weather.py b/homeassistant/components/nws/weather.py index 60f93f20177..4684714d58c 100644 --- a/homeassistant/components/nws/weather.py +++ b/homeassistant/components/nws/weather.py @@ -25,8 +25,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.dt import utcnow -from homeassistant.util.speed import convert as convert_speed -from homeassistant.util.temperature import convert as convert_temperature +from homeassistant.util.unit_conversion import SpeedConverter, TemperatureConverter from . import base_unique_id, device_info from .const import ( @@ -235,7 +234,7 @@ class NWSWeather(WeatherEntity): } if (temp := forecast_entry.get("temperature")) is not None: - data[ATTR_FORECAST_NATIVE_TEMP] = convert_temperature( + data[ATTR_FORECAST_NATIVE_TEMP] = TemperatureConverter.convert( temp, TEMP_FAHRENHEIT, TEMP_CELSIUS ) else: @@ -255,7 +254,7 @@ class NWSWeather(WeatherEntity): data[ATTR_FORECAST_WIND_BEARING] = forecast_entry.get("windBearing") wind_speed = forecast_entry.get("windSpeedAvg") if wind_speed is not None: - data[ATTR_FORECAST_NATIVE_WIND_SPEED] = convert_speed( + data[ATTR_FORECAST_NATIVE_WIND_SPEED] = SpeedConverter.convert( wind_speed, SPEED_MILES_PER_HOUR, SPEED_KILOMETERS_PER_HOUR ) else: @@ -269,7 +268,7 @@ class NWSWeather(WeatherEntity): return f"{base_unique_id(self.latitude, self.longitude)}_{self.mode}" @property - def available(self): + def available(self) -> bool: """Return if state is available.""" last_success = ( self.coordinator_observation.last_update_success @@ -289,7 +288,7 @@ class NWSWeather(WeatherEntity): last_success_time = False return last_success or last_success_time - async def async_update(self): + async def async_update(self) -> None: """Update the entity. Only used by the generic entity update service. diff --git a/homeassistant/components/nzbget/switch.py b/homeassistant/components/nzbget/switch.py index 4e4cca34aa8..74b49b63501 100644 --- a/homeassistant/components/nzbget/switch.py +++ b/homeassistant/components/nzbget/switch.py @@ -1,6 +1,8 @@ """Support for NZBGet switches.""" from __future__ import annotations +from typing import Any + from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME @@ -61,12 +63,12 @@ class NZBGetDownloadSwitch(NZBGetEntity, SwitchEntity): """Return the state of the switch.""" return not self.coordinator.data["status"].get("DownloadPaused", False) - async def async_turn_on(self, **kwargs) -> None: + async def async_turn_on(self, **kwargs: Any) -> None: """Set downloads to enabled.""" await self.hass.async_add_executor_job(self.coordinator.nzbget.resumedownload) await self.coordinator.async_request_refresh() - async def async_turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs: Any) -> None: """Set downloads to paused.""" await self.hass.async_add_executor_job(self.coordinator.nzbget.pausedownload) await self.coordinator.async_request_refresh() diff --git a/homeassistant/components/oasa_telematics/sensor.py b/homeassistant/components/oasa_telematics/sensor.py index 47bdd24d192..3cb624190e7 100644 --- a/homeassistant/components/oasa_telematics/sensor.py +++ b/homeassistant/components/oasa_telematics/sensor.py @@ -127,7 +127,7 @@ class OASATelematicsSensor(SensorEntity): """Icon to use in the frontend, if any.""" return ICON - def update(self): + def update(self) -> None: """Get the latest data from OASA API and update the states.""" self.data.update() self._times = self.data.info diff --git a/homeassistant/components/obihai/sensor.py b/homeassistant/components/obihai/sensor.py index 1b1dc339af1..cff4e6232e7 100644 --- a/homeassistant/components/obihai/sensor.py +++ b/homeassistant/components/obihai/sensor.py @@ -146,7 +146,7 @@ class ObihaiServiceSensors(SensorEntity): return "mdi:restart-alert" return "mdi:phone" - def update(self): + def update(self) -> None: """Update the sensor.""" services = self._pyobihai.get_state() diff --git a/homeassistant/components/octoprint/translations/cs.json b/homeassistant/components/octoprint/translations/cs.json index fa519dfe6e1..c2e27a5efb0 100644 --- a/homeassistant/components/octoprint/translations/cs.json +++ b/homeassistant/components/octoprint/translations/cs.json @@ -1,14 +1,19 @@ { "config": { "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, "step": { "user": { "data": { + "host": "Hostitel", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no", "verify_ssl": "Ov\u011b\u0159it certifik\u00e1t SSL" } } diff --git a/homeassistant/components/oem/climate.py b/homeassistant/components/oem/climate.py index 298ae965e17..d6337807fc2 100644 --- a/homeassistant/components/oem/climate.py +++ b/homeassistant/components/oem/climate.py @@ -1,12 +1,15 @@ """OpenEnergyMonitor Thermostat Support.""" from __future__ import annotations +from typing import Any + from oemthermostat import Thermostat import requests import voluptuous as vol -from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( + PLATFORM_SCHEMA, + ClimateEntity, ClimateEntityFeature, HVACAction, HVACMode, @@ -122,12 +125,12 @@ class ThermostatDevice(ClimateEntity): elif hvac_mode == HVACMode.OFF: self.thermostat.mode = 0 - def set_temperature(self, **kwargs): + def set_temperature(self, **kwargs: Any) -> None: """Set the temperature.""" temp = kwargs.get(ATTR_TEMPERATURE) self.thermostat.setpoint = temp - def update(self): + def update(self) -> None: """Update local state.""" self._setpoint = self.thermostat.setpoint self._temperature = self.thermostat.temperature diff --git a/homeassistant/components/ohmconnect/sensor.py b/homeassistant/components/ohmconnect/sensor.py index 09105984a31..11606bfc6c2 100644 --- a/homeassistant/components/ohmconnect/sensor.py +++ b/homeassistant/components/ohmconnect/sensor.py @@ -51,6 +51,7 @@ class OhmconnectSensor(SensorEntity): self._name = name self._ohmid = ohmid self._data = {} + self._attr_unique_id = ohmid @property def name(self): @@ -70,7 +71,7 @@ class OhmconnectSensor(SensorEntity): return {"Address": self._data.get("address"), "ID": self._ohmid} @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): + def update(self) -> None: """Get the latest data from OhmConnect.""" try: url = f"https://login.ohmconnect.com/verify-ohm-hour/{self._ohmid}" diff --git a/homeassistant/components/ombi/sensor.py b/homeassistant/components/ombi/sensor.py index 9152af0f1bf..90410ea8da2 100644 --- a/homeassistant/components/ombi/sensor.py +++ b/homeassistant/components/ombi/sensor.py @@ -45,7 +45,7 @@ class OmbiSensor(SensorEntity): self._attr_name = f"Ombi {description.name}" - def update(self): + def update(self) -> None: """Update the sensor.""" try: sensor_type = self.entity_description.key diff --git a/homeassistant/components/onboarding/manifest.json b/homeassistant/components/onboarding/manifest.json index fe65d82f626..4e200d22502 100644 --- a/homeassistant/components/onboarding/manifest.json +++ b/homeassistant/components/onboarding/manifest.json @@ -5,5 +5,6 @@ "after_dependencies": ["hassio"], "dependencies": ["analytics", "auth", "http", "person"], "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "system" } diff --git a/homeassistant/components/onboarding/views.py b/homeassistant/components/onboarding/views.py index c29fb7edf3a..43d942c8912 100644 --- a/homeassistant/components/onboarding/views.py +++ b/homeassistant/components/onboarding/views.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant.auth.const import GROUP_ID_ADMIN from homeassistant.components import person from homeassistant.components.auth import indieauth -from homeassistant.components.http.const import KEY_HASS_REFRESH_TOKEN_ID +from homeassistant.components.http import KEY_HASS_REFRESH_TOKEN_ID from homeassistant.components.http.data_validator import RequestDataValidator from homeassistant.components.http.view import HomeAssistantView from homeassistant.core import callback diff --git a/homeassistant/components/onewire/translations/cs.json b/homeassistant/components/onewire/translations/cs.json index ae22bc1ce25..05c02505642 100644 --- a/homeassistant/components/onewire/translations/cs.json +++ b/homeassistant/components/onewire/translations/cs.json @@ -8,6 +8,9 @@ }, "step": { "user": { + "data": { + "host": "Hostitel" + }, "title": "Nastaven\u00ed 1-Wire" } } diff --git a/homeassistant/components/onkyo/media_player.py b/homeassistant/components/onkyo/media_player.py index 381f48b0b16..c1d242c840c 100644 --- a/homeassistant/components/onkyo/media_player.py +++ b/homeassistant/components/onkyo/media_player.py @@ -2,24 +2,20 @@ from __future__ import annotations import logging +from typing import Any import eiscp from eiscp import eISCP import voluptuous as vol from homeassistant.components.media_player import ( + DOMAIN, PLATFORM_SCHEMA, MediaPlayerEntity, MediaPlayerEntityFeature, + MediaPlayerState, ) -from homeassistant.components.media_player.const import DOMAIN -from homeassistant.const import ( - ATTR_ENTITY_ID, - CONF_HOST, - CONF_NAME, - STATE_OFF, - STATE_ON, -) +from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST, CONF_NAME from homeassistant.core import HomeAssistant, ServiceCall import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -258,7 +254,7 @@ class OnkyoDevice(MediaPlayerEntity): self._receiver = receiver self._muted = False self._volume = 0 - self._pwstate = STATE_OFF + self._pwstate = MediaPlayerState.OFF if name: # not discovered self._name = name @@ -295,16 +291,16 @@ class OnkyoDevice(MediaPlayerEntity): _LOGGER.debug("Result for %s: %s", command, result) return result - def update(self): + def update(self) -> None: """Get the latest state from the device.""" status = self.command("system-power query") if not status: return if status[1] == "on": - self._pwstate = STATE_ON + self._pwstate = MediaPlayerState.ON else: - self._pwstate = STATE_OFF + self._pwstate = MediaPlayerState.OFF self._attributes.pop(ATTR_AUDIO_INFORMATION, None) self._attributes.pop(ATTR_VIDEO_INFORMATION, None) self._attributes.pop(ATTR_PRESET, None) @@ -396,11 +392,11 @@ class OnkyoDevice(MediaPlayerEntity): """Return device specific state attributes.""" return self._attributes - def turn_off(self): + def turn_off(self) -> None: """Turn the media player off.""" self.command("system-power standby") - def set_volume_level(self, volume): + def set_volume_level(self, volume: float) -> None: """ Set volume level, input is range 0..1. @@ -414,32 +410,32 @@ class OnkyoDevice(MediaPlayerEntity): f"volume {int(volume * (self._max_volume / 100) * self._receiver_max_volume)}" ) - def volume_up(self): + def volume_up(self) -> None: """Increase volume by 1 step.""" self.command("volume level-up") - def volume_down(self): + def volume_down(self) -> None: """Decrease volume by 1 step.""" self.command("volume level-down") - def mute_volume(self, mute): + def mute_volume(self, mute: bool) -> None: """Mute (true) or unmute (false) media player.""" if mute: self.command("audio-muting on") else: self.command("audio-muting off") - def turn_on(self): + def turn_on(self) -> None: """Turn the media player on.""" self.command("system-power on") - def select_source(self, source): + def select_source(self, source: str) -> None: """Set the input source.""" if source in self._source_list: source = self._reverse_mapping[source] self.command(f"input-selector {source}") - def play_media(self, media_type, media_id, **kwargs): + def play_media(self, media_type: str, media_id: str, **kwargs: Any) -> None: """Play radio station by preset number.""" source = self._reverse_mapping[self._current_source] if media_type.lower() == "radio" and source in DEFAULT_PLAYABLE_SOURCES: @@ -506,16 +502,16 @@ class OnkyoDeviceZone(OnkyoDevice): self._supports_volume = True super().__init__(receiver, sources, name, max_volume, receiver_max_volume) - def update(self): + def update(self) -> None: """Get the latest state from the device.""" status = self.command(f"zone{self._zone}.power=query") if not status: return if status[1] == "on": - self._pwstate = STATE_ON + self._pwstate = MediaPlayerState.ON else: - self._pwstate = STATE_OFF + self._pwstate = MediaPlayerState.OFF return volume_raw = self.command(f"zone{self._zone}.volume=query") @@ -563,11 +559,11 @@ class OnkyoDeviceZone(OnkyoDevice): return SUPPORT_ONKYO return SUPPORT_ONKYO_WO_VOLUME - def turn_off(self): + def turn_off(self) -> None: """Turn the media player off.""" self.command(f"zone{self._zone}.power=standby") - def set_volume_level(self, volume): + def set_volume_level(self, volume: float) -> None: """ Set volume level, input is range 0..1. @@ -581,26 +577,26 @@ class OnkyoDeviceZone(OnkyoDevice): f"zone{self._zone}.volume={int(volume * (self._max_volume / 100) * self._receiver_max_volume)}" ) - def volume_up(self): + def volume_up(self) -> None: """Increase volume by 1 step.""" self.command(f"zone{self._zone}.volume=level-up") - def volume_down(self): + def volume_down(self) -> None: """Decrease volume by 1 step.""" self.command(f"zone{self._zone}.volume=level-down") - def mute_volume(self, mute): + def mute_volume(self, mute: bool) -> None: """Mute (true) or unmute (false) media player.""" if mute: self.command(f"zone{self._zone}.muting=on") else: self.command(f"zone{self._zone}.muting=off") - def turn_on(self): + def turn_on(self) -> None: """Turn the media player on.""" self.command(f"zone{self._zone}.power=on") - def select_source(self, source): + def select_source(self, source: str) -> None: """Set the input source.""" if source in self._source_list: source = self._reverse_mapping[source] diff --git a/homeassistant/components/onvif/binary_sensor.py b/homeassistant/components/onvif/binary_sensor.py index cd9af1d83b5..2a7f23c61ee 100644 --- a/homeassistant/components/onvif/binary_sensor.py +++ b/homeassistant/components/onvif/binary_sensor.py @@ -82,7 +82,7 @@ class ONVIFBinarySensor(ONVIFBaseEntity, RestoreEntity, BinarySensorEntity): return event.value return self._attr_is_on - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Connect to dispatcher listening for entity data notifications.""" self.async_on_remove( self.device.events.async_add_listener(self.async_write_ha_state) diff --git a/homeassistant/components/onvif/camera.py b/homeassistant/components/onvif/camera.py index 528b9605bbc..9a8535f2599 100644 --- a/homeassistant/components/onvif/camera.py +++ b/homeassistant/components/onvif/camera.py @@ -12,8 +12,8 @@ from homeassistant.components.ffmpeg import CONF_EXTRA_ARGUMENTS, get_ffmpeg_man from homeassistant.components.stream import ( CONF_RTSP_TRANSPORT, CONF_USE_WALLCLOCK_AS_TIMESTAMPS, + RTSP_TRANSPORTS, ) -from homeassistant.components.stream.const import RTSP_TRANSPORTS from homeassistant.config_entries import ConfigEntry from homeassistant.const import HTTP_BASIC_AUTHENTICATION from homeassistant.core import HomeAssistant @@ -109,7 +109,7 @@ class ONVIFCameraEntity(ONVIFBaseEntity, Camera): device.config_entry.data.get(CONF_SNAPSHOT_AUTH) == HTTP_BASIC_AUTHENTICATION ) - self._stream_uri = None + self._stream_uri: str | None = None @property def name(self) -> str: @@ -142,16 +142,21 @@ class ONVIFCameraEntity(ONVIFBaseEntity, Camera): if self.device.capabilities.snapshot: try: - image = await self.device.device.get_snapshot( + if image := await self.device.device.get_snapshot( self.profile.token, self._basic_auth - ) - return image + ): + return image except ONVIFError as err: LOGGER.error( "Fetch snapshot image failed from %s, falling back to FFmpeg; %s", self.device.name, err, ) + else: + LOGGER.error( + "Fetch snapshot image failed from %s, falling back to FFmpeg", + self.device.name, + ) assert self._stream_uri return await ffmpeg.async_get_image( @@ -185,7 +190,7 @@ class ONVIFCameraEntity(ONVIFBaseEntity, Camera): finally: await stream.close() - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Run when entity about to be added to hass.""" uri_no_auth = await self.device.async_get_stream_uri(self.profile) url = URL(uri_no_auth) diff --git a/homeassistant/components/onvif/device.py b/homeassistant/components/onvif/device.py index dda28e07a2a..d7f50f6744e 100644 --- a/homeassistant/components/onvif/device.py +++ b/homeassistant/components/onvif/device.py @@ -74,12 +74,12 @@ class ONVIFDevice: return self.config_entry.data[CONF_PORT] @property - def username(self) -> int: + def username(self) -> str: """Return the username of this device.""" return self.config_entry.data[CONF_USERNAME] @property - def password(self) -> int: + def password(self) -> str: """Return the password of this device.""" return self.config_entry.data[CONF_PASSWORD] diff --git a/homeassistant/components/onvif/sensor.py b/homeassistant/components/onvif/sensor.py index b662dca1d5d..4ad4c11f175 100644 --- a/homeassistant/components/onvif/sensor.py +++ b/homeassistant/components/onvif/sensor.py @@ -81,7 +81,7 @@ class ONVIFSensor(ONVIFBaseEntity, RestoreSensor): return event.value return self._attr_native_value - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Connect to dispatcher listening for entity data notifications.""" self.async_on_remove( self.device.events.async_add_listener(self.async_write_ha_state) diff --git a/homeassistant/components/openalpr_local/image_processing.py b/homeassistant/components/openalpr_local/image_processing.py index ef8c942189f..0237bc1f60c 100644 --- a/homeassistant/components/openalpr_local/image_processing.py +++ b/homeassistant/components/openalpr_local/image_processing.py @@ -12,6 +12,7 @@ from homeassistant.components.image_processing import ( ATTR_CONFIDENCE, CONF_CONFIDENCE, PLATFORM_SCHEMA, + ImageProcessingDeviceClass, ImageProcessingEntity, ) from homeassistant.const import ( @@ -102,9 +103,11 @@ async def async_setup_platform( class ImageProcessingAlprEntity(ImageProcessingEntity): """Base entity class for ALPR image processing.""" - def __init__(self): + _attr_device_class = ImageProcessingDeviceClass.ALPR + + def __init__(self) -> None: """Initialize base ALPR entity.""" - self.plates = {} + self.plates: dict[str, float] = {} self.vehicles = 0 @property @@ -120,35 +123,30 @@ class ImageProcessingAlprEntity(ImageProcessingEntity): plate = i_pl return plate - @property - def device_class(self): - """Return the class of this device, from component DEVICE_CLASSES.""" - return "alpr" - @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, 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, vehicles): + 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: - { 'plate': confidence } + { '': confidence } This method must be run in the event loop. """ plates = { plate: confidence for plate, confidence in plates.items() - if confidence >= self.confidence + if self.confidence is None or confidence >= self.confidence } new_plates = set(plates) - set(self.plates) diff --git a/homeassistant/components/openerz/sensor.py b/homeassistant/components/openerz/sensor.py index c83611a303c..4bfe11ee264 100644 --- a/homeassistant/components/openerz/sensor.py +++ b/homeassistant/components/openerz/sensor.py @@ -58,7 +58,7 @@ class OpenERZSensor(SensorEntity): """Return the state of the sensor.""" return self._state - def update(self): + def update(self) -> None: """Fetch new state data for the sensor. This is the only method that should fetch new data for Home Assistant. diff --git a/homeassistant/components/openevse/manifest.json b/homeassistant/components/openevse/manifest.json index 3a8984af253..37ab8c4c031 100644 --- a/homeassistant/components/openevse/manifest.json +++ b/homeassistant/components/openevse/manifest.json @@ -2,7 +2,7 @@ "domain": "openevse", "name": "OpenEVSE", "documentation": "https://www.home-assistant.io/integrations/openevse", - "requirements": ["openevsewifi==1.1.0"], + "requirements": ["openevsewifi==1.1.2"], "codeowners": [], "iot_class": "local_polling", "loggers": ["openevsewifi"] diff --git a/homeassistant/components/openevse/sensor.py b/homeassistant/components/openevse/sensor.py index 3dcea4d0126..5bb469a3d99 100644 --- a/homeassistant/components/openevse/sensor.py +++ b/homeassistant/components/openevse/sensor.py @@ -118,7 +118,7 @@ class OpenEVSESensor(SensorEntity): self.entity_description = description self.charger = charger - def update(self): + def update(self) -> None: """Get the monitored data from the charger.""" try: sensor_type = self.entity_description.key diff --git a/homeassistant/components/openexchangerates/translations/bg.json b/homeassistant/components/openexchangerates/translations/bg.json new file mode 100644 index 00000000000..6c5c49465c3 --- /dev/null +++ b/homeassistant/components/openexchangerates/translations/bg.json @@ -0,0 +1,22 @@ +{ + "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", + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "user": { + "data": { + "api_key": "API \u043a\u043b\u044e\u0447", + "base": "\u041e\u0441\u043d\u043e\u0432\u043d\u0430 \u0432\u0430\u043b\u0443\u0442\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openexchangerates/translations/cs.json b/homeassistant/components/openexchangerates/translations/cs.json new file mode 100644 index 00000000000..af472a5e3d1 --- /dev/null +++ b/homeassistant/components/openexchangerates/translations/cs.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9", + "timeout_connect": "Vypr\u0161el \u010dasov\u00fd limit pro nav\u00e1z\u00e1n\u00ed spojen\u00ed" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "timeout_connect": "Vypr\u0161el \u010dasov\u00fd limit pro nav\u00e1z\u00e1n\u00ed spojen\u00ed", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "api_key": "Kl\u00ed\u010d API" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openexchangerates/translations/es.json b/homeassistant/components/openexchangerates/translations/es.json index fb5897846db..c5cf1b266e2 100644 --- a/homeassistant/components/openexchangerates/translations/es.json +++ b/homeassistant/components/openexchangerates/translations/es.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "El servicio ya est\u00e1 configurado", "cannot_connect": "No se pudo conectar", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente", "timeout_connect": "Tiempo de espera agotado para establecer la conexi\u00f3n" }, "error": { diff --git a/homeassistant/components/openexchangerates/translations/fr.json b/homeassistant/components/openexchangerates/translations/fr.json index a6b5929245a..2e8b1c42cac 100644 --- a/homeassistant/components/openexchangerates/translations/fr.json +++ b/homeassistant/components/openexchangerates/translations/fr.json @@ -23,5 +23,10 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "title": "La configuration YAML pour Open Exchange Rates sera bient\u00f4t supprim\u00e9e" + } } } \ No newline at end of file diff --git a/homeassistant/components/openexchangerates/translations/id.json b/homeassistant/components/openexchangerates/translations/id.json index 0b5f0183e2b..29f4a6eaabf 100644 --- a/homeassistant/components/openexchangerates/translations/id.json +++ b/homeassistant/components/openexchangerates/translations/id.json @@ -19,7 +19,7 @@ "base": "Mata uang dasar" }, "data_description": { - "base": "Menggunakan mata uang dasar selain USD memerlukan [paket berbayar]({daftar})." + "base": "Menggunakan mata uang dasar selain USD memerlukan [paket berbayar]({signup})." } } } diff --git a/homeassistant/components/openexchangerates/translations/pt.json b/homeassistant/components/openexchangerates/translations/pt.json new file mode 100644 index 00000000000..1da8a0cc5ab --- /dev/null +++ b/homeassistant/components/openexchangerates/translations/pt.json @@ -0,0 +1,20 @@ +{ + "config": { + "step": { + "user": { + "data": { + "base": "Moeda base" + }, + "data_description": { + "base": "Usar outra moeda base que n\u00e3o seja USD requer um [plano pago]( {signup} )." + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "A configura\u00e7\u00e3o de taxas de c\u00e2mbio abertas usando YAML est\u00e1 sendo removida. \n\n Sua configura\u00e7\u00e3o YAML existente foi importada para a interface do usu\u00e1rio automaticamente. \n\n Remova a configura\u00e7\u00e3o Open Exchange Rates YAML do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "A configura\u00e7\u00e3o de YAML de taxas de c\u00e2mbio abertas est\u00e1 sendo removida" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openexchangerates/translations/sv.json b/homeassistant/components/openexchangerates/translations/sv.json new file mode 100644 index 00000000000..578d74864ba --- /dev/null +++ b/homeassistant/components/openexchangerates/translations/sv.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_configured": "Tj\u00e4nsten \u00e4r redan konfigurerad", + "cannot_connect": "Det gick inte att ansluta.", + "reauth_successful": "\u00c5terautentisering lyckades", + "timeout_connect": "Timeout uppr\u00e4ttar anslutning" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "timeout_connect": "Timeout uppr\u00e4ttar anslutning", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "data": { + "api_key": "API-nyckel", + "base": "Basvaluta" + }, + "data_description": { + "base": "Att anv\u00e4nda en annan basvaluta \u00e4n USD kr\u00e4ver en [betald plan]( {signup} )." + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Konfigurering av Open Exchange Rates med YAML tas bort. \n\n Din befintliga YAML-konfiguration har automatiskt importerats till anv\u00e4ndargr\u00e4nssnittet. \n\n Ta bort Open Exchange Rates YAML-konfigurationen fr\u00e5n filen configuration.yaml och starta om Home Assistant f\u00f6r att \u00e5tg\u00e4rda problemet.", + "title": "Open Exchange Rates YAML-konfigurationen tas bort" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/opengarage/sensor.py b/homeassistant/components/opengarage/sensor.py index d09ed130152..bf75cd34998 100644 --- a/homeassistant/components/opengarage/sensor.py +++ b/homeassistant/components/opengarage/sensor.py @@ -29,6 +29,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="dist", native_unit_of_measurement=LENGTH_CENTIMETERS, + device_class=SensorDeviceClass.DISTANCE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( diff --git a/homeassistant/components/openhardwaremonitor/sensor.py b/homeassistant/components/openhardwaremonitor/sensor.py index f21126d01a0..70dbbd38fc8 100644 --- a/homeassistant/components/openhardwaremonitor/sensor.py +++ b/homeassistant/components/openhardwaremonitor/sensor.py @@ -91,7 +91,7 @@ class OpenHardwareMonitorDevice(SensorEntity): """In some locales a decimal numbers uses ',' instead of '.'.""" return string.replace(",", ".") - def update(self): + def update(self) -> None: """Update the device from a new JSON object.""" self._data.update() diff --git a/homeassistant/components/openhome/media_player.py b/homeassistant/components/openhome/media_player.py index b6a0b549c40..f352d7101ac 100644 --- a/homeassistant/components/openhome/media_player.py +++ b/homeassistant/components/openhome/media_player.py @@ -15,14 +15,13 @@ import voluptuous as vol from homeassistant.components import media_source from homeassistant.components.media_player import ( + BrowseMedia, MediaPlayerEntity, MediaPlayerEntityFeature, -) -from homeassistant.components.media_player.browse_media import ( + MediaPlayerState, + MediaType, async_process_play_media_url, ) -from homeassistant.components.media_player.const import MEDIA_TYPE_MUSIC -from homeassistant.const import STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -125,7 +124,7 @@ class OpenhomeDevice(MediaPlayerEntity): self._source_index = {} self._source = {} self._name = None - self._state = STATE_PLAYING + self._state = MediaPlayerState.PLAYING self._available = True @property @@ -133,7 +132,7 @@ class OpenhomeDevice(MediaPlayerEntity): """Device is available.""" return self._available - async def async_update(self): + async def async_update(self) -> None: """Update state of device.""" try: self._in_standby = await self._device.is_in_standby() @@ -179,46 +178,48 @@ class OpenhomeDevice(MediaPlayerEntity): ) if self._in_standby: - self._state = STATE_OFF + self._state = MediaPlayerState.OFF elif self._transport_state == "Paused": - self._state = STATE_PAUSED + self._state = MediaPlayerState.PAUSED elif self._transport_state in ("Playing", "Buffering"): - self._state = STATE_PLAYING + self._state = MediaPlayerState.PLAYING elif self._transport_state == "Stopped": - self._state = STATE_IDLE + self._state = MediaPlayerState.IDLE else: # Device is playing an external source with no transport controls - self._state = STATE_PLAYING + self._state = MediaPlayerState.PLAYING self._available = True except (asyncio.TimeoutError, aiohttp.ClientError, UpnpError): self._available = False @catch_request_errors() - async def async_turn_on(self): + async def async_turn_on(self) -> None: """Bring device out of standby.""" await self._device.set_standby(False) @catch_request_errors() - async def async_turn_off(self): + async def async_turn_off(self) -> None: """Put device in standby.""" await self._device.set_standby(True) @catch_request_errors() - async def async_play_media(self, media_type, media_id, **kwargs): + async def async_play_media( + self, media_type: str, media_id: str, **kwargs: Any + ) -> None: """Send the play_media command to the media player.""" if media_source.is_media_source_id(media_id): - media_type = MEDIA_TYPE_MUSIC + media_type = MediaType.MUSIC play_item = await media_source.async_resolve_media( self.hass, media_id, self.entity_id ) media_id = play_item.url - if media_type != MEDIA_TYPE_MUSIC: + if media_type != MediaType.MUSIC: _LOGGER.error( "Invalid media type %s. Only %s is supported", media_type, - MEDIA_TYPE_MUSIC, + MediaType.MUSIC, ) return @@ -228,32 +229,32 @@ class OpenhomeDevice(MediaPlayerEntity): await self._device.play_media(track_details) @catch_request_errors() - async def async_media_pause(self): + async def async_media_pause(self) -> None: """Send pause command.""" await self._device.pause() @catch_request_errors() - async def async_media_stop(self): + async def async_media_stop(self) -> None: """Send stop command.""" await self._device.stop() @catch_request_errors() - async def async_media_play(self): + async def async_media_play(self) -> None: """Send play command.""" await self._device.play() @catch_request_errors() - async def async_media_next_track(self): + async def async_media_next_track(self) -> None: """Send next track command.""" await self._device.skip(1) @catch_request_errors() - async def async_media_previous_track(self): + async def async_media_previous_track(self) -> None: """Send previous track command.""" await self._device.skip(-1) @catch_request_errors() - async def async_select_source(self, source): + async def async_select_source(self, source: str) -> None: """Select input source.""" await self._device.set_source(self._source_index[source]) @@ -325,26 +326,28 @@ class OpenhomeDevice(MediaPlayerEntity): return self._volume_muted @catch_request_errors() - async def async_volume_up(self): + async def async_volume_up(self) -> None: """Volume up media player.""" await self._device.increase_volume() @catch_request_errors() - async def async_volume_down(self): + async def async_volume_down(self) -> None: """Volume down media player.""" await self._device.decrease_volume() @catch_request_errors() - async def async_set_volume_level(self, volume): + async def async_set_volume_level(self, volume: float) -> None: """Set volume level, range 0..1.""" await self._device.set_volume(int(volume * 100)) @catch_request_errors() - async def async_mute_volume(self, mute): + async def async_mute_volume(self, mute: bool) -> None: """Mute (true) or unmute (false) media player.""" await self._device.set_mute(mute) - 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.""" return await media_source.async_browse_media( self.hass, diff --git a/homeassistant/components/opensky/sensor.py b/homeassistant/components/opensky/sensor.py index b4278bcce36..66579eb8173 100644 --- a/homeassistant/components/opensky/sensor.py +++ b/homeassistant/components/opensky/sensor.py @@ -22,7 +22,8 @@ from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from homeassistant.util import distance as util_distance, location as util_location +from homeassistant.util import location as util_location +from homeassistant.util.unit_conversion import DistanceConverter CONF_ALTITUDE = "altitude" @@ -105,7 +106,9 @@ class OpenSkySensor(SensorEntity): self._session = requests.Session() self._latitude = latitude self._longitude = longitude - self._radius = util_distance.convert(radius, LENGTH_KILOMETERS, LENGTH_METERS) + self._radius = DistanceConverter.convert( + radius, LENGTH_KILOMETERS, LENGTH_METERS + ) self._altitude = altitude self._state = 0 self._hass = hass @@ -147,7 +150,7 @@ class OpenSkySensor(SensorEntity): } self._hass.bus.fire(event, data) - def update(self): + def update(self) -> None: """Update device state.""" currently_tracked = set() flight_metadata = {} @@ -159,18 +162,17 @@ class OpenSkySensor(SensorEntity): flight_metadata[callsign] = flight else: continue - missing_location = ( - flight.get(ATTR_LONGITUDE) is None or flight.get(ATTR_LATITUDE) is None - ) - if missing_location: - continue - if flight.get(ATTR_ON_GROUND): + if ( + (longitude := flight.get(ATTR_LONGITUDE)) is None + or (latitude := flight.get(ATTR_LATITUDE)) is None + or flight.get(ATTR_ON_GROUND) + ): continue distance = util_location.distance( self._latitude, self._longitude, - flight.get(ATTR_LATITUDE), - flight.get(ATTR_LONGITUDE), + latitude, + longitude, ) if distance is None or distance > self._radius: continue diff --git a/homeassistant/components/opentherm_gw/binary_sensor.py b/homeassistant/components/opentherm_gw/binary_sensor.py index 083fb103481..194e047e50e 100644 --- a/homeassistant/components/opentherm_gw/binary_sensor.py +++ b/homeassistant/components/opentherm_gw/binary_sensor.py @@ -102,14 +102,14 @@ class OpenThermBinarySensor(BinarySensorEntity): self._friendly_name = friendly_name_format.format(gw_dev.name) self._unsub_updates = None - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Subscribe to updates from the component.""" _LOGGER.debug("Added OpenTherm Gateway binary sensor %s", self._friendly_name) self._unsub_updates = async_dispatcher_connect( self.hass, self._gateway.update_signal, self.receive_report ) - async def async_will_remove_from_hass(self): + async def async_will_remove_from_hass(self) -> None: """Unsubscribe from updates from the component.""" _LOGGER.debug( "Removing OpenTherm Gateway binary sensor %s", self._friendly_name diff --git a/homeassistant/components/opentherm_gw/climate.py b/homeassistant/components/opentherm_gw/climate.py index a805cbacba0..08cfbf68ac8 100644 --- a/homeassistant/components/opentherm_gw/climate.py +++ b/homeassistant/components/opentherm_gw/climate.py @@ -2,13 +2,15 @@ from __future__ import annotations import logging +from typing import Any from pyotgw import vars as gw_vars -from homeassistant.components.climate import ENTITY_ID_FORMAT, ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( + ENTITY_ID_FORMAT, PRESET_AWAY, PRESET_NONE, + ClimateEntity, ClimateEntityFeature, HVACAction, HVACMode, @@ -101,7 +103,7 @@ class OpenThermClimate(ClimateEntity): self.temporary_ovrd_mode = entry.options[CONF_TEMPORARY_OVRD_MODE] self.async_write_ha_state() - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Connect to the OpenTherm Gateway device.""" _LOGGER.debug("Added OpenTherm Gateway climate device %s", self.friendly_name) self._unsub_updates = async_dispatcher_connect( @@ -111,7 +113,7 @@ class OpenThermClimate(ClimateEntity): self.hass, self._gateway.options_update_signal, self.update_options ) - async def async_will_remove_from_hass(self): + async def async_will_remove_from_hass(self) -> None: """Unsubscribe from updates from the component.""" _LOGGER.debug("Removing OpenTherm Gateway climate %s", self.friendly_name) self._unsub_options() @@ -261,11 +263,11 @@ class OpenThermClimate(ClimateEntity): """Available preset modes to set.""" return [] - def set_preset_mode(self, preset_mode): + def set_preset_mode(self, preset_mode: str) -> None: """Set the preset mode.""" _LOGGER.warning("Changing preset mode is not supported") - async def async_set_temperature(self, **kwargs): + async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" if ATTR_TEMPERATURE in kwargs: temp = float(kwargs[ATTR_TEMPERATURE]) diff --git a/homeassistant/components/opentherm_gw/sensor.py b/homeassistant/components/opentherm_gw/sensor.py index 5eea4fca099..67b4eb138dd 100644 --- a/homeassistant/components/opentherm_gw/sensor.py +++ b/homeassistant/components/opentherm_gw/sensor.py @@ -106,14 +106,14 @@ class OpenThermSensor(SensorEntity): self._friendly_name = friendly_name_format.format(gw_dev.name) self._unsub_updates = None - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Subscribe to updates from the component.""" _LOGGER.debug("Added OpenTherm Gateway sensor %s", self._friendly_name) self._unsub_updates = async_dispatcher_connect( self.hass, self._gateway.update_signal, self.receive_report ) - async def async_will_remove_from_hass(self): + async def async_will_remove_from_hass(self) -> None: """Unsubscribe from updates from the component.""" _LOGGER.debug("Removing OpenTherm Gateway sensor %s", self._friendly_name) self._unsub_updates() diff --git a/homeassistant/components/opentherm_gw/translations/cs.json b/homeassistant/components/opentherm_gw/translations/cs.json index 5bf8d4fc385..6177aba85eb 100644 --- a/homeassistant/components/opentherm_gw/translations/cs.json +++ b/homeassistant/components/opentherm_gw/translations/cs.json @@ -3,7 +3,8 @@ "error": { "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", - "id_exists": "ID br\u00e1ny ji\u017e existuje" + "id_exists": "ID br\u00e1ny ji\u017e existuje", + "timeout_connect": "Vypr\u0161el \u010dasov\u00fd limit pro nav\u00e1z\u00e1n\u00ed spojen\u00ed" }, "step": { "init": { diff --git a/homeassistant/components/openuv/__init__.py b/homeassistant/components/openuv/__init__.py index 29b13ff5258..365a3ab247a 100644 --- a/homeassistant/components/openuv/__init__.py +++ b/homeassistant/components/openuv/__init__.py @@ -2,12 +2,14 @@ from __future__ import annotations import asyncio +from collections.abc import Callable from typing import Any from pyopenuv import Client from pyopenuv.errors import OpenUvError +import voluptuous as vol -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.const import ( CONF_API_KEY, CONF_BINARY_SENSORS, @@ -19,12 +21,18 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError -from homeassistant.helpers import aiohttp_client +from homeassistant.helpers import ( + aiohttp_client, + config_validation as cv, + entity_registry, +) +from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) from homeassistant.helpers.entity import Entity, EntityDescription +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.service import verify_domain_control from .const import ( @@ -38,15 +46,81 @@ from .const import ( LOGGER, ) -DEFAULT_ATTRIBUTION = "Data provided by OpenUV" +CONF_ENTRY_ID = "entry_id" -NOTIFICATION_ID = "openuv_notification" -NOTIFICATION_TITLE = "OpenUV Component Setup" +DEFAULT_DEBOUNCER_COOLDOWN_SECONDS = 15 * 60 TOPIC_UPDATE = f"{DOMAIN}_data_update" PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR] +SERVICE_NAME_UPDATE_DATA = "update_data" +SERVICE_NAME_UPDATE_PROTECTION_DATA = "update_protection_data" +SERVICE_NAME_UPDATE_UV_INDEX_DATA = "update_uv_index_data" + +SERVICES = ( + SERVICE_NAME_UPDATE_DATA, + SERVICE_NAME_UPDATE_PROTECTION_DATA, + SERVICE_NAME_UPDATE_UV_INDEX_DATA, +) + + +@callback +def async_get_entity_id_from_unique_id_suffix( + hass: HomeAssistant, entry: ConfigEntry, unique_id_suffix: str +) -> str: + """Get the entity ID for a config entry based on unique ID suffix.""" + ent_reg = entity_registry.async_get(hass) + [registry_entry] = [ + registry_entry + for registry_entry in ent_reg.entities.values() + if registry_entry.config_entry_id == entry.entry_id + and registry_entry.unique_id.endswith(unique_id_suffix) + ] + return registry_entry.entity_id + + +@callback +def async_log_deprecated_service_call( + hass: HomeAssistant, + call: ServiceCall, + alternate_service: str, + alternate_targets: list[str], + breaks_in_ha_version: str, +) -> None: + """Log a warning about a deprecated service call.""" + deprecated_service = f"{call.domain}.{call.service}" + + if len(alternate_targets) > 1: + translation_key = "deprecated_service_multiple_alternate_targets" + else: + translation_key = "deprecated_service_single_alternate_target" + + async_create_issue( + hass, + DOMAIN, + f"deprecated_service_{deprecated_service}", + breaks_in_ha_version=breaks_in_ha_version, + is_fixable=False, + is_persistent=True, + severity=IssueSeverity.WARNING, + translation_key=translation_key, + translation_placeholders={ + "alternate_service": alternate_service, + "alternate_targets": ", ".join(alternate_targets), + "deprecated_service": deprecated_service, + }, + ) + + LOGGER.warning( + ( + 'The "%s" service is deprecated and will be removed in %s; review the ' + "Repairs item in the UI for more information" + ), + deprecated_service, + breaks_in_ha_version, + ) + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up OpenUV as config entry.""" @@ -54,6 +128,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: websession = aiohttp_client.async_get_clientsession(hass) openuv = OpenUV( + hass, entry, Client( entry.data[CONF_API_KEY], @@ -82,33 +157,90 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + @callback + def extract_openuv(func: Callable) -> Callable: + """Define a decorator to get the correct OpenUV object for a service call.""" + + async def wrapper(call: ServiceCall) -> None: + """Wrap the service function.""" + openuv: OpenUV = hass.data[DOMAIN][call.data[CONF_ENTRY_ID]] + + try: + await func(call, openuv) + except OpenUvError as err: + raise HomeAssistantError( + f'Error while executing "{call.service}": {err}' + ) from err + + return wrapper + + # We determine entity IDs needed to help the user migrate from deprecated services: + current_uv_index_entity_id = async_get_entity_id_from_unique_id_suffix( + hass, entry, "current_uv_index" + ) + protection_window_entity_id = async_get_entity_id_from_unique_id_suffix( + hass, entry, "protection_window" + ) + @_verify_domain_control - async def update_data(_: ServiceCall) -> None: + @extract_openuv + async def update_data(call: ServiceCall, openuv: OpenUV) -> None: """Refresh all OpenUV data.""" LOGGER.debug("Refreshing all OpenUV data") + async_log_deprecated_service_call( + hass, + call, + "homeassistant.update_entity", + [protection_window_entity_id, current_uv_index_entity_id], + "2022.12.0", + ) await openuv.async_update() async_dispatcher_send(hass, TOPIC_UPDATE) @_verify_domain_control - async def update_uv_index_data(_: ServiceCall) -> None: + @extract_openuv + async def update_uv_index_data(call: ServiceCall, openuv: OpenUV) -> None: """Refresh OpenUV UV index data.""" LOGGER.debug("Refreshing OpenUV UV index data") + async_log_deprecated_service_call( + hass, + call, + "homeassistant.update_entity", + [current_uv_index_entity_id], + "2022.12.0", + ) await openuv.async_update_uv_index_data() async_dispatcher_send(hass, TOPIC_UPDATE) @_verify_domain_control - async def update_protection_data(_: ServiceCall) -> None: + @extract_openuv + async def update_protection_data(call: ServiceCall, openuv: OpenUV) -> None: """Refresh OpenUV protection window data.""" LOGGER.debug("Refreshing OpenUV protection window data") + async_log_deprecated_service_call( + hass, + call, + "homeassistant.update_entity", + [protection_window_entity_id], + "2022.12.0", + ) await openuv.async_update_protection_data() async_dispatcher_send(hass, TOPIC_UPDATE) + service_schema = vol.Schema( + { + vol.Optional(CONF_ENTRY_ID, default=entry.entry_id): cv.string, + } + ) + for service, method in ( - ("update_data", update_data), - ("update_uv_index_data", update_uv_index_data), - ("update_protection_data", update_protection_data), + (SERVICE_NAME_UPDATE_DATA, update_data), + (SERVICE_NAME_UPDATE_UV_INDEX_DATA, update_uv_index_data), + (SERVICE_NAME_UPDATE_PROTECTION_DATA, update_protection_data), ): - hass.services.async_register(DOMAIN, service, method) + if hass.services.has_service(DOMAIN, service): + continue + hass.services.async_register(DOMAIN, service, method, schema=service_schema) return True @@ -119,6 +251,17 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) + loaded_entries = [ + entry + for entry in hass.config_entries.async_entries(DOMAIN) + if entry.state == ConfigEntryState.LOADED + ] + if len(loaded_entries) == 1: + # If this is the last loaded instance of OpenUV, deregister any services + # defined during integration setup: + for service_name in SERVICES: + hass.services.async_remove(DOMAIN, service_name) + return unload_ok @@ -143,13 +286,29 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: class OpenUV: """Define a generic OpenUV object.""" - def __init__(self, entry: ConfigEntry, client: Client) -> None: + def __init__(self, hass: HomeAssistant, entry: ConfigEntry, client: Client) -> None: """Initialize.""" + self._update_protection_data_debouncer = Debouncer( + hass, + LOGGER, + cooldown=DEFAULT_DEBOUNCER_COOLDOWN_SECONDS, + immediate=True, + function=self._async_update_protection_data, + ) + + self._update_uv_index_data_debouncer = Debouncer( + hass, + LOGGER, + cooldown=DEFAULT_DEBOUNCER_COOLDOWN_SECONDS, + immediate=True, + function=self._async_update_uv_index_data, + ) + self._entry = entry self.client = client self.data: dict[str, Any] = {DATA_PROTECTION_WINDOW: {}, DATA_UV: {}} - async def async_update_protection_data(self) -> None: + async def _async_update_protection_data(self) -> None: """Update binary sensor (protection window) data.""" low = self._entry.options.get(CONF_FROM_WINDOW, DEFAULT_FROM_WINDOW) high = self._entry.options.get(CONF_TO_WINDOW, DEFAULT_TO_WINDOW) @@ -163,7 +322,7 @@ class OpenUV: self.data[DATA_PROTECTION_WINDOW] = data.get("result") - async def async_update_uv_index_data(self) -> None: + async def _async_update_uv_index_data(self) -> None: """Update sensor (uv index, etc) data.""" try: data = await self.client.uv_index() @@ -174,6 +333,14 @@ class OpenUV: self.data[DATA_UV] = data.get("result") + async def async_update_protection_data(self) -> None: + """Update binary sensor (protection window) data with a debouncer.""" + await self._update_protection_data_debouncer.async_call() + + async def async_update_uv_index_data(self) -> None: + """Update sensor (uv index, etc) data with a debouncer.""" + await self._update_uv_index_data_debouncer.async_call() + async def async_update(self) -> None: """Update sensor/binary sensor data.""" tasks = [self.async_update_protection_data(), self.async_update_uv_index_data()] @@ -195,18 +362,26 @@ class OpenUvEntity(Entity): self.entity_description = description self.openuv = openuv + @callback + def async_update_state(self) -> None: + """Update the state.""" + self.update_from_latest_data() + self.async_write_ha_state() + async def async_added_to_hass(self) -> None: """Register callbacks.""" - - @callback - def update() -> None: - """Update the state.""" - self.update_from_latest_data() - self.async_write_ha_state() - - self.async_on_remove(async_dispatcher_connect(self.hass, TOPIC_UPDATE, update)) - self.update_from_latest_data() + self.async_on_remove( + async_dispatcher_connect(self.hass, TOPIC_UPDATE, self.async_update_state) + ) + + async def async_update(self) -> None: + """Update the entity. + + Only used by the generic entity update service. Should be implemented by each + OpenUV platform. + """ + raise NotImplementedError def update_from_latest_data(self) -> None: """Update the sensor using the latest data.""" diff --git a/homeassistant/components/openuv/binary_sensor.py b/homeassistant/components/openuv/binary_sensor.py index 757f0479e01..b1c962932b7 100644 --- a/homeassistant/components/openuv/binary_sensor.py +++ b/homeassistant/components/openuv/binary_sensor.py @@ -36,6 +36,14 @@ async def async_setup_entry( class OpenUvBinarySensor(OpenUvEntity, BinarySensorEntity): """Define a binary sensor for OpenUV.""" + async def async_update(self) -> None: + """Update the entity. + + Only used by the generic entity update service. + """ + await self.openuv.async_update_protection_data() + self.async_update_state() + @callback def update_from_latest_data(self) -> None: """Update the state.""" diff --git a/homeassistant/components/openuv/sensor.py b/homeassistant/components/openuv/sensor.py index 3a5bd3c2a47..ff28062da37 100644 --- a/homeassistant/components/openuv/sensor.py +++ b/homeassistant/components/openuv/sensor.py @@ -131,6 +131,14 @@ async def async_setup_entry( class OpenUvSensor(OpenUvEntity, SensorEntity): """Define a binary sensor for OpenUV.""" + async def async_update(self) -> None: + """Update the entity. + + Only used by the generic entity update service. + """ + await self.openuv.async_update_uv_index_data() + self.async_update_state() + @callback def update_from_latest_data(self) -> None: """Update the state.""" diff --git a/homeassistant/components/openuv/services.yaml b/homeassistant/components/openuv/services.yaml index e4886dfa7d8..3e2e6ab0087 100644 --- a/homeassistant/components/openuv/services.yaml +++ b/homeassistant/components/openuv/services.yaml @@ -2,11 +2,35 @@ update_data: name: Update data description: Request new data from OpenUV. Consumes two API calls. + fields: + entry_id: + name: Config Entry + description: The configured instance of the OpenUV integration to use + required: true + selector: + config_entry: + integration: openuv update_uv_index_data: name: Update UV index data description: Request new UV index data from OpenUV. + fields: + entry_id: + name: Config Entry + description: The configured instance of the OpenUV integration to use + required: true + selector: + config_entry: + integration: openuv update_protection_data: name: Update protection data description: Request new protection window data from OpenUV. + fields: + entry_id: + name: Config Entry + description: The configured instance of the OpenUV integration to use + required: true + selector: + config_entry: + integration: openuv diff --git a/homeassistant/components/openuv/strings.json b/homeassistant/components/openuv/strings.json index cd9ec36d93a..84a093280f3 100644 --- a/homeassistant/components/openuv/strings.json +++ b/homeassistant/components/openuv/strings.json @@ -28,5 +28,15 @@ } } } + }, + "issues": { + "deprecated_service_multiple_alternate_targets": { + "title": "The {deprecated_service} service is being removed", + "description": "Update any automations or scripts that use this service to instead use the `{alternate_service}` service with one of these entity IDs as the target: `{alternate_targets}`." + }, + "deprecated_service_single_alternate_target": { + "title": "The {deprecated_service} service is being removed", + "description": "Update any automations or scripts that use this service to instead use the `{alternate_service}` service with `{alternate_targets}` as the target." + } } } diff --git a/homeassistant/components/openuv/translations/bg.json b/homeassistant/components/openuv/translations/bg.json index 1bfee97d1e4..6959a04bb7a 100644 --- a/homeassistant/components/openuv/translations/bg.json +++ b/homeassistant/components/openuv/translations/bg.json @@ -18,6 +18,14 @@ } } }, + "issues": { + "deprecated_service_multiple_alternate_targets": { + "title": "\u0423\u0441\u043b\u0443\u0433\u0430\u0442\u0430 {deprecated_service} \u0441\u0435 \u043f\u0440\u0435\u043c\u0430\u0445\u0432\u0430" + }, + "deprecated_service_single_alternate_target": { + "title": "\u0423\u0441\u043b\u0443\u0433\u0430\u0442\u0430 {deprecated_service} \u0441\u0435 \u043f\u0440\u0435\u043c\u0430\u0445\u0432\u0430" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/openuv/translations/ca.json b/homeassistant/components/openuv/translations/ca.json index df55a79d1a5..36043c3bde3 100644 --- a/homeassistant/components/openuv/translations/ca.json +++ b/homeassistant/components/openuv/translations/ca.json @@ -18,6 +18,16 @@ } } }, + "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}`.", + "title": "El servei {deprecated_service} est\u00e0 sent eliminat" + }, + "deprecated_service_single_alternate_target": { + "description": "Actualitza totes les automatitzacions o 'scripts' que utilitzin aquest servei perqu\u00e8 passin a utilitzar el servei `{alternate_service}` amb `{alternate_targets}` com a objectius o 'targets'.", + "title": "El servei {deprecated_service} est\u00e0 sent eliminat" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/openuv/translations/de.json b/homeassistant/components/openuv/translations/de.json index abc32f68f02..94d8b49b7d5 100644 --- a/homeassistant/components/openuv/translations/de.json +++ b/homeassistant/components/openuv/translations/de.json @@ -18,6 +18,16 @@ } } }, + "issues": { + "deprecated_service_multiple_alternate_targets": { + "description": "Aktualisiere alle Automatisierungen oder Skripte, die diesen Dienst verwenden, um stattdessen den Dienst `{alternate_service}` mit einer dieser Entit\u00e4ts-IDs als Ziel zu verwenden: `{alternate_targets}`.", + "title": "Der Dienst {deprecated_service} wird entfernt" + }, + "deprecated_service_single_alternate_target": { + "description": "Aktualisiere alle Automatisierungen oder Skripte, die diesen Dienst verwenden, um stattdessen den Dienst `{alternate_service}` mit `{alternate_targets}` als Ziel zu verwenden.", + "title": "Der Dienst {deprecated_service} wird entfernt" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/openuv/translations/el.json b/homeassistant/components/openuv/translations/el.json index c86a90e1f09..0b81ff25fd5 100644 --- a/homeassistant/components/openuv/translations/el.json +++ b/homeassistant/components/openuv/translations/el.json @@ -18,6 +18,16 @@ } } }, + "issues": { + "deprecated_service_multiple_alternate_targets": { + "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 \u03ad\u03bd\u03b1 \u03b1\u03c0\u03cc \u03b1\u03c5\u03c4\u03ac \u03c4\u03b1 \u03b1\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03ac \u03bf\u03bd\u03c4\u03bf\u03c4\u03ae\u03c4\u03c9\u03bd \u03c9\u03c2 \u03c3\u03c4\u03cc\u03c7\u03bf: `{alternate_targets}`.", + "title": "\u0397 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 {deprecated_service} \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9" + }, + "deprecated_service_single_alternate_target": { + "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 \u03c3\u03c4\u03cc\u03c7\u03bf `{alternate_targets}`.", + "title": "\u0397 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 {deprecated_service} \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/openuv/translations/en.json b/homeassistant/components/openuv/translations/en.json index 92ca71cd46f..3879a4d7d44 100644 --- a/homeassistant/components/openuv/translations/en.json +++ b/homeassistant/components/openuv/translations/en.json @@ -18,6 +18,16 @@ } } }, + "issues": { + "deprecated_service_multiple_alternate_targets": { + "description": "Update any automations or scripts that use this service to instead use the `{alternate_service}` service with one of these entity IDs as the target: `{alternate_targets}`.", + "title": "The {deprecated_service} service is being removed" + }, + "deprecated_service_single_alternate_target": { + "description": "Update any automations or scripts that use this service to instead use the `{alternate_service}` service with `{alternate_targets}` as the target.", + "title": "The {deprecated_service} service is being removed" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/openuv/translations/es.json b/homeassistant/components/openuv/translations/es.json index bb5f36499ba..66331b8e5a5 100644 --- a/homeassistant/components/openuv/translations/es.json +++ b/homeassistant/components/openuv/translations/es.json @@ -18,6 +18,16 @@ } } }, + "issues": { + "deprecated_service_multiple_alternate_targets": { + "description": "Actualiza cualquier automatizaci\u00f3n o script que use este servicio para usar en su lugar el servicio `{alternate_service}` con uno de estos ID de entidad como objetivo: `{alternate_targets}`.", + "title": "Se va a eliminar el servicio {deprecated_service}" + }, + "deprecated_service_single_alternate_target": { + "description": "Actualiza cualquier automatizaci\u00f3n o script que use este servicio para usar en su lugar el servicio `{alternate_service}` con `{alternate_targets}` como destino.", + "title": "Se va a eliminar el servicio {deprecated_service}" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/openuv/translations/et.json b/homeassistant/components/openuv/translations/et.json index 89bfcd38318..76145f40ed0 100644 --- a/homeassistant/components/openuv/translations/et.json +++ b/homeassistant/components/openuv/translations/et.json @@ -18,6 +18,16 @@ } } }, + "issues": { + "deprecated_service_multiple_alternate_targets": { + "description": "V\u00e4rskenda k\u00f5iki automaatikaid v\u00f5i skripte, mis seda teenust kasutavad, et kasutada selle asemel teenust '{alternate_service}', mille sihtm\u00e4rgiks on \u00fcks neist olemi ID-dest: '{alternate_targets}'.", + "title": "Teenus {deprecated_service} eemaldatakse" + }, + "deprecated_service_single_alternate_target": { + "description": "Uuenda k\u00f5iki seda teenust kasutavaid automatiseerimisi v\u00f5i skripte, et need kasutaksid selle asemel teenust `{alternate_service}}, mille sihtm\u00e4rgiks on `{alternate_targets}}.", + "title": "Teenus {deprecated_service} eemaldatakse" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/openuv/translations/fr.json b/homeassistant/components/openuv/translations/fr.json index 99a616427a7..527d4d3348e 100644 --- a/homeassistant/components/openuv/translations/fr.json +++ b/homeassistant/components/openuv/translations/fr.json @@ -18,6 +18,14 @@ } } }, + "issues": { + "deprecated_service_multiple_alternate_targets": { + "title": "Le service {deprecated_service} sera bient\u00f4t supprim\u00e9" + }, + "deprecated_service_single_alternate_target": { + "title": "Le service {deprecated_service} sera bient\u00f4t supprim\u00e9" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/openuv/translations/hu.json b/homeassistant/components/openuv/translations/hu.json index e94d076782d..f6493247a99 100644 --- a/homeassistant/components/openuv/translations/hu.json +++ b/homeassistant/components/openuv/translations/hu.json @@ -18,6 +18,16 @@ } } }, + "issues": { + "deprecated_service_multiple_alternate_targets": { + "description": "Friss\u00edtsen minden olyan automatizmust vagy szkriptet, amely ezt a szolg\u00e1ltat\u00e1st haszn\u00e1lja, hogy ehelyett az `{alternate_service}` szolg\u00e1ltat\u00e1st haszn\u00e1lja c\u00e9lpontk\u00e9nt az al\u00e1bbi entit\u00e1sazonos\u00edt\u00f3k egyik\u00e9vel: `{alternate_targets}`.", + "title": "A {deprecated_service} szolg\u00e1ltat\u00e1s elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" + }, + "deprecated_service_single_alternate_target": { + "description": "Friss\u00edtsen minden olyan automatizmust vagy szkriptet, amely ezt a szolg\u00e1ltat\u00e1st haszn\u00e1lja, hogy helyette az `{alternate_service}` szolg\u00e1ltat\u00e1st haszn\u00e1lja, a `{alternate_targets}` c\u00e9lpontk\u00e9nt.", + "title": "{deprecated_service} szolg\u00e1ltat\u00e1s elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/openuv/translations/id.json b/homeassistant/components/openuv/translations/id.json index 568e92d1999..6ad5ee1f8d9 100644 --- a/homeassistant/components/openuv/translations/id.json +++ b/homeassistant/components/openuv/translations/id.json @@ -18,6 +18,16 @@ } } }, + "issues": { + "deprecated_service_multiple_alternate_targets": { + "description": "Perbarui semua otomasi atau skrip yang menggunakan layanan ini untuk menggunakan layanan `{alternate_service}` dengan salah satu ID entitas berikut sebagai target: `{alternate_targets}`.", + "title": "Layanan {deprecated_service} dalam proses penghapusan" + }, + "deprecated_service_single_alternate_target": { + "description": "Perbarui semua otomasi atau skrip yang menggunakan layanan ini untuk menggunakan layanan `{alternate_service}` dengan `{alternate_targets}` sebagai target.", + "title": "Layanan {deprecated_service} dalam proses penghapusan" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/openuv/translations/it.json b/homeassistant/components/openuv/translations/it.json index 241e118a800..4e51b09aec2 100644 --- a/homeassistant/components/openuv/translations/it.json +++ b/homeassistant/components/openuv/translations/it.json @@ -18,6 +18,16 @@ } } }, + "issues": { + "deprecated_service_multiple_alternate_targets": { + "description": "Aggiorna tutte le automazioni o gli script che utilizzano questo servizio per utilizzare invece il servizio `{alternate_service}` con uno di questi ID entit\u00e0 come destinazione: `{alternate_targets}`.", + "title": "Il servizio {deprecated_service} \u00e8 stato rimosso" + }, + "deprecated_service_single_alternate_target": { + "description": "Aggiorna tutte le automazioni o gli script che utilizzano questo servizio per utilizzare invece il servizio `{alternate_service}` con `{alternate_targets}` come destinazione.", + "title": "Il servizio {deprecated_service} \u00e8 stato rimosso" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/openuv/translations/no.json b/homeassistant/components/openuv/translations/no.json index f76787b4e4d..1fcba27dc9f 100644 --- a/homeassistant/components/openuv/translations/no.json +++ b/homeassistant/components/openuv/translations/no.json @@ -18,6 +18,16 @@ } } }, + "issues": { + "deprecated_service_multiple_alternate_targets": { + "description": "Oppdater eventuelle automatiseringer eller skript som bruker denne tjenesten til i stedet \u00e5 bruke ` {alternate_service} `-tjenesten med en av disse enhets-ID-ene som m\u00e5l: ` {alternate_targets} `.", + "title": "{deprecated_service} -tjenesten blir fjernet" + }, + "deprecated_service_single_alternate_target": { + "description": "Oppdater eventuelle automatiseringer eller skript som bruker denne tjenesten til i stedet \u00e5 bruke ` {alternate_service} `-tjenesten med ` {alternate_targets} ` som m\u00e5l.", + "title": "{deprecated_service} -tjenesten blir fjernet" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/openuv/translations/pl.json b/homeassistant/components/openuv/translations/pl.json index 6aff15beef1..6578e6fcf84 100644 --- a/homeassistant/components/openuv/translations/pl.json +++ b/homeassistant/components/openuv/translations/pl.json @@ -18,6 +18,16 @@ } } }, + "issues": { + "deprecated_service_multiple_alternate_targets": { + "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 jednym z tych identyfikator\u00f3w encji jako docelow\u0105: `{alternate_targets}`.", + "title": "Us\u0142uga {deprecated_service} zostanie usuni\u0119ta" + }, + "deprecated_service_single_alternate_target": { + "description": "Zaktualizuj wszystkie automatyzacje lub skrypty korzystaj\u0105ce z tej us\u0142ugi, aby zamiast tego u\u017cywa\u0142y us\u0142ugi `{alternate_service}` z `{alternate_targets}` jako docelow\u0105.", + "title": "Us\u0142uga {deprecated_service} zostanie usuni\u0119ta" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/openuv/translations/pt-BR.json b/homeassistant/components/openuv/translations/pt-BR.json index 1fe9216bdad..9d0c6dd7e8a 100644 --- a/homeassistant/components/openuv/translations/pt-BR.json +++ b/homeassistant/components/openuv/translations/pt-BR.json @@ -18,6 +18,16 @@ } } }, + "issues": { + "deprecated_service_multiple_alternate_targets": { + "description": "Atualize quaisquer automa\u00e7\u00f5es ou scripts que usam este servi\u00e7o para usar o servi\u00e7o `{alternate_service}` com um destes IDs de entidade como destino: `{alternate_targets}`.", + "title": "O servi\u00e7o {deprecated_service} est\u00e1 sendo removido" + }, + "deprecated_service_single_alternate_target": { + "description": "Atualize quaisquer automa\u00e7\u00f5es ou scripts que usam este servi\u00e7o para usar o servi\u00e7o `{alternate_service}` com `{alternate_targets}` como destino.", + "title": "O servi\u00e7o {deprecated_service} est\u00e1 sendo removido" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/openuv/translations/ru.json b/homeassistant/components/openuv/translations/ru.json index e4a2fe9f49c..ce7da6af147 100644 --- a/homeassistant/components/openuv/translations/ru.json +++ b/homeassistant/components/openuv/translations/ru.json @@ -18,6 +18,16 @@ } } }, + "issues": { + "deprecated_service_multiple_alternate_targets": { + "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_targets}`.", + "title": "\u0421\u043b\u0443\u0436\u0431\u0430 {deprecated_service} \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430" + }, + "deprecated_service_single_alternate_target": { + "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_targets}`.", + "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/openuv/translations/zh-Hant.json b/homeassistant/components/openuv/translations/zh-Hant.json index 16d083da340..eaeeec74e3c 100644 --- a/homeassistant/components/openuv/translations/zh-Hant.json +++ b/homeassistant/components/openuv/translations/zh-Hant.json @@ -18,6 +18,16 @@ } } }, + "issues": { + "deprecated_service_multiple_alternate_targets": { + "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 \u4e4b\u4e00\u70ba `{alternate_target}` \u4e4b `{alternate_service}` \u670d\u52d9\u3002", + "title": "{deprecated_service} \u670d\u52d9\u5373\u5c07\u79fb\u9664" + }, + "deprecated_service_single_alternate_target": { + "description": "\u4f7f\u7528\u6b64\u670d\u52d9\u4ee5\u66f4\u65b0\u4efb\u4f55\u81ea\u52d5\u5316\u6216\u8173\u672c\u3001\u4ee5\u53d6\u4ee3\u76ee\u6a19\u70ba `{alternate_target}` \u4e4b `{alternate_service}` \u670d\u52d9\u3002", + "title": "{deprecated_service} \u670d\u52d9\u5373\u5c07\u79fb\u9664" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/openweathermap/weather_update_coordinator.py b/homeassistant/components/openweathermap/weather_update_coordinator.py index 98c3b56fb5e..630250b4701 100644 --- a/homeassistant/components/openweathermap/weather_update_coordinator.py +++ b/homeassistant/components/openweathermap/weather_update_coordinator.py @@ -9,10 +9,11 @@ from homeassistant.components.weather import ( ATTR_CONDITION_CLEAR_NIGHT, ATTR_CONDITION_SUNNY, ) +from homeassistant.const import TEMP_CELSIUS, TEMP_KELVIN from homeassistant.helpers import sun from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.util import dt -from homeassistant.util.temperature import kelvin_to_celsius +from homeassistant.util.unit_conversion import TemperatureConverter from .const import ( ATTR_API_CLOUDS, @@ -191,7 +192,9 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): def _fmt_dewpoint(dewpoint): """Format the dewpoint data.""" if dewpoint is not None: - return round(kelvin_to_celsius(dewpoint), 1) + return round( + TemperatureConverter.convert(dewpoint, TEMP_KELVIN, TEMP_CELSIUS), 1 + ) return None @staticmethod diff --git a/homeassistant/components/opnsense/device_tracker.py b/homeassistant/components/opnsense/device_tracker.py index e726a0484f9..b5c75f1cc21 100644 --- a/homeassistant/components/opnsense/device_tracker.py +++ b/homeassistant/components/opnsense/device_tracker.py @@ -1,4 +1,6 @@ """Device tracker support for OPNSense routers.""" +from __future__ import annotations + from homeassistant.components.device_tracker import DeviceScanner from homeassistant.core import HomeAssistant from homeassistant.helpers.typing import ConfigType @@ -6,7 +8,9 @@ from homeassistant.helpers.typing import ConfigType from . import CONF_TRACKER_INTERFACE, OPNSENSE_DATA -async def async_get_scanner(hass: HomeAssistant, config: ConfigType) -> DeviceScanner: +async def async_get_scanner( + hass: HomeAssistant, config: ConfigType +) -> OPNSenseDeviceScanner: """Configure the OPNSense device_tracker.""" interface_client = hass.data[OPNSENSE_DATA]["interfaces"] scanner = OPNSenseDeviceScanner( diff --git a/homeassistant/components/opple/light.py b/homeassistant/components/opple/light.py index 25bcf821fc9..80d0f8630dc 100644 --- a/homeassistant/components/opple/light.py +++ b/homeassistant/components/opple/light.py @@ -69,7 +69,7 @@ class OppleLight(LightEntity): self._color_temp = None @property - def available(self): + def available(self) -> bool: """Return True if light is available.""" return self._device.is_online diff --git a/homeassistant/components/oru/sensor.py b/homeassistant/components/oru/sensor.py index b31defd7171..5d99e782fdd 100644 --- a/homeassistant/components/oru/sensor.py +++ b/homeassistant/components/oru/sensor.py @@ -75,7 +75,7 @@ class CurrentEnergyUsageSensor(SensorEntity): """Return the state of the sensor.""" return self._state - def update(self): + def update(self) -> None: """Fetch new state data for the sensor.""" try: last_read = self.meter.last_read() diff --git a/homeassistant/components/orvibo/switch.py b/homeassistant/components/orvibo/switch.py index b526b9057bd..9e86d3787af 100644 --- a/homeassistant/components/orvibo/switch.py +++ b/homeassistant/components/orvibo/switch.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any from orvibo.s20 import S20, S20Exception, discover import voluptuous as vol @@ -93,21 +94,21 @@ class S20Switch(SwitchEntity): """Return true if device is on.""" return self._state - def update(self): + def update(self) -> None: """Update device state.""" try: self._state = self._s20.on except self._exc: _LOGGER.exception("Error while fetching S20 state") - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" try: self._s20.on = True except self._exc: _LOGGER.exception("Error while turning on S20") - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" try: self._s20.on = False diff --git a/homeassistant/components/otp/manifest.json b/homeassistant/components/otp/manifest.json index 0c16e660aa9..7916e3cb4b7 100644 --- a/homeassistant/components/otp/manifest.json +++ b/homeassistant/components/otp/manifest.json @@ -2,7 +2,7 @@ "domain": "otp", "name": "One-Time Password (OTP)", "documentation": "https://www.home-assistant.io/integrations/otp", - "requirements": ["pyotp==2.6.0"], + "requirements": ["pyotp==2.7.0"], "codeowners": [], "quality_scale": "internal", "iot_class": "local_polling", diff --git a/homeassistant/components/otp/sensor.py b/homeassistant/components/otp/sensor.py index ff5a2965795..499c9b129f1 100644 --- a/homeassistant/components/otp/sensor.py +++ b/homeassistant/components/otp/sensor.py @@ -53,7 +53,7 @@ class TOTPSensor(SensorEntity): self._state = None self._next_expiration = None - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Handle when an entity is about to be added to Home Assistant.""" self._call_loop() diff --git a/homeassistant/components/overkiz/button.py b/homeassistant/components/overkiz/button.py index 8118d0fd23e..a4c2bc3c2da 100644 --- a/homeassistant/components/overkiz/button.py +++ b/homeassistant/components/overkiz/button.py @@ -1,6 +1,10 @@ """Support for Overkiz (virtual) buttons.""" from __future__ import annotations +from dataclasses import dataclass + +from pyoverkiz.types import StateType as OverkizStateType + from homeassistant.components.button import ButtonEntity, ButtonEntityDescription from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -11,41 +15,56 @@ from . import HomeAssistantOverkizData from .const import DOMAIN, IGNORED_OVERKIZ_DEVICES from .entity import OverkizDescriptiveEntity -BUTTON_DESCRIPTIONS: list[ButtonEntityDescription] = [ + +@dataclass +class OverkizButtonDescription(ButtonEntityDescription): + """Class to describe an Overkiz button.""" + + press_args: OverkizStateType | None = None + + +BUTTON_DESCRIPTIONS: list[OverkizButtonDescription] = [ # My Position (cover, light) - ButtonEntityDescription( + OverkizButtonDescription( key="my", - name="My Position", + name="My position", icon="mdi:star", ), # Identify - ButtonEntityDescription( + OverkizButtonDescription( key="identify", # startIdentify and identify are reversed... Swap this when fixed in API. - name="Start Identify", + name="Start identify", icon="mdi:human-greeting-variant", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), - ButtonEntityDescription( + OverkizButtonDescription( key="stopIdentify", - name="Stop Identify", + name="Stop identify", icon="mdi:human-greeting-variant", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), - ButtonEntityDescription( + OverkizButtonDescription( key="startIdentify", # startIdentify and identify are reversed... Swap this when fixed in API. name="Identify", icon="mdi:human-greeting-variant", entity_category=EntityCategory.DIAGNOSTIC, ), # RTDIndoorSiren / RTDOutdoorSiren - ButtonEntityDescription(key="dingDong", name="Ding Dong", icon="mdi:bell-ring"), - ButtonEntityDescription(key="bip", name="Bip", icon="mdi:bell-ring"), - ButtonEntityDescription( - key="fastBipSequence", name="Fast Bip Sequence", icon="mdi:bell-ring" + OverkizButtonDescription(key="dingDong", name="Ding dong", icon="mdi:bell-ring"), + OverkizButtonDescription(key="bip", name="Bip", icon="mdi:bell-ring"), + OverkizButtonDescription( + key="fastBipSequence", name="Fast bip sequence", icon="mdi:bell-ring" + ), + OverkizButtonDescription(key="ring", name="Ring", icon="mdi:bell-ring"), + # DynamicScreen (ogp:blind) uses goToAlias (id 1: favorite1) instead of 'my' + OverkizButtonDescription( + key="goToAlias", + press_args="1", + name="My position", + icon="mdi:star", ), - ButtonEntityDescription(key="ring", name="Ring", icon="mdi:bell-ring"), ] SUPPORTED_COMMANDS = { @@ -85,6 +104,14 @@ async def async_setup_entry( class OverkizButton(OverkizDescriptiveEntity, ButtonEntity): """Representation of an Overkiz Button.""" + entity_description: OverkizButtonDescription + async def async_press(self) -> None: """Handle the button press.""" + if self.entity_description.press_args: + await self.executor.async_execute_command( + self.entity_description.key, self.entity_description.press_args + ) + return + await self.executor.async_execute_command(self.entity_description.key) diff --git a/homeassistant/components/overkiz/climate_entities/__init__.py b/homeassistant/components/overkiz/climate_entities/__init__.py index 737ea342c40..32fae234be1 100644 --- a/homeassistant/components/overkiz/climate_entities/__init__.py +++ b/homeassistant/components/overkiz/climate_entities/__init__.py @@ -3,12 +3,14 @@ from pyoverkiz.enums.ui import UIWidget from .atlantic_electrical_heater import AtlanticElectricalHeater from .atlantic_electrical_towel_dryer import AtlanticElectricalTowelDryer +from .atlantic_heat_recovery_ventilation import AtlanticHeatRecoveryVentilation from .atlantic_pass_apc_zone_control import AtlanticPassAPCZoneControl from .somfy_thermostat import SomfyThermostat WIDGET_TO_CLIMATE_ENTITY = { UIWidget.ATLANTIC_ELECTRICAL_HEATER: AtlanticElectricalHeater, UIWidget.ATLANTIC_ELECTRICAL_TOWEL_DRYER: AtlanticElectricalTowelDryer, + UIWidget.ATLANTIC_HEAT_RECOVERY_VENTILATION: AtlanticHeatRecoveryVentilation, UIWidget.ATLANTIC_PASS_APC_ZONE_CONTROL: AtlanticPassAPCZoneControl, UIWidget.SOMFY_THERMOSTAT: SomfyThermostat, } diff --git a/homeassistant/components/overkiz/climate_entities/atlantic_electrical_heater.py b/homeassistant/components/overkiz/climate_entities/atlantic_electrical_heater.py index 0a397e9f2ad..c0d1dd04663 100644 --- a/homeassistant/components/overkiz/climate_entities/atlantic_electrical_heater.py +++ b/homeassistant/components/overkiz/climate_entities/atlantic_electrical_heater.py @@ -5,17 +5,18 @@ from typing import cast from pyoverkiz.enums import OverkizCommand, OverkizCommandParam, OverkizState -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( PRESET_COMFORT, PRESET_ECO, PRESET_NONE, + ClimateEntity, ClimateEntityFeature, HVACMode, ) -from homeassistant.components.overkiz.entity import OverkizEntity from homeassistant.const import TEMP_CELSIUS +from ..entity import OverkizEntity + PRESET_FROST_PROTECTION = "frost_protection" OVERKIZ_TO_HVAC_MODES: dict[str, HVACMode] = { diff --git a/homeassistant/components/overkiz/climate_entities/atlantic_electrical_towel_dryer.py b/homeassistant/components/overkiz/climate_entities/atlantic_electrical_towel_dryer.py index 9a24a7bf1a9..7ab59a47a34 100644 --- a/homeassistant/components/overkiz/climate_entities/atlantic_electrical_towel_dryer.py +++ b/homeassistant/components/overkiz/climate_entities/atlantic_electrical_towel_dryer.py @@ -5,17 +5,18 @@ from typing import Any, cast from pyoverkiz.enums import OverkizCommand, OverkizCommandParam, OverkizState -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( PRESET_BOOST, PRESET_NONE, + ClimateEntity, ClimateEntityFeature, HVACMode, ) -from homeassistant.components.overkiz.coordinator import OverkizDataUpdateCoordinator -from homeassistant.components.overkiz.entity import OverkizEntity from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS +from ..coordinator import OverkizDataUpdateCoordinator +from ..entity import OverkizEntity + PRESET_DRYING = "drying" OVERKIZ_TO_HVAC_MODE: dict[str, str] = { diff --git a/homeassistant/components/overkiz/climate_entities/atlantic_heat_recovery_ventilation.py b/homeassistant/components/overkiz/climate_entities/atlantic_heat_recovery_ventilation.py new file mode 100644 index 00000000000..3627aa21c43 --- /dev/null +++ b/homeassistant/components/overkiz/climate_entities/atlantic_heat_recovery_ventilation.py @@ -0,0 +1,176 @@ +"""Support for AtlanticHeatRecoveryVentilation.""" +from __future__ import annotations + +from typing import cast + +from pyoverkiz.enums import OverkizCommand, OverkizCommandParam, OverkizState + +from homeassistant.components.climate import ( + FAN_AUTO, + ClimateEntity, + ClimateEntityFeature, + HVACMode, +) +from homeassistant.const import TEMP_CELSIUS + +from ..coordinator import OverkizDataUpdateCoordinator +from ..entity import OverkizEntity + +FAN_BOOST = "home_boost" +FAN_KITCHEN = "kitchen_boost" +FAN_AWAY = "away" +FAN_BYPASS = "bypass_boost" + +PRESET_AUTO = "auto" +PRESET_PROG = "prog" +PRESET_MANUAL = "manual" + +OVERKIZ_TO_FAN_MODES: dict[str, str] = { + OverkizCommandParam.AUTO: FAN_AUTO, + OverkizCommandParam.AWAY: FAN_AWAY, + OverkizCommandParam.BOOST: FAN_BOOST, + OverkizCommandParam.HIGH: FAN_KITCHEN, + "": FAN_BYPASS, +} + +FAN_MODES_TO_OVERKIZ = {v: k for k, v in OVERKIZ_TO_FAN_MODES.items()} + +TEMPERATURE_SENSOR_DEVICE_INDEX = 4 + + +class AtlanticHeatRecoveryVentilation(OverkizEntity, ClimateEntity): + """Representation of a AtlanticHeatRecoveryVentilation device.""" + + _attr_fan_modes = [*FAN_MODES_TO_OVERKIZ] + _attr_hvac_mode = HVACMode.FAN_ONLY + _attr_hvac_modes = [HVACMode.FAN_ONLY] + _attr_preset_modes = [PRESET_AUTO, PRESET_PROG, PRESET_MANUAL] + _attr_temperature_unit = TEMP_CELSIUS + _attr_supported_features = ( + ClimateEntityFeature.PRESET_MODE | ClimateEntityFeature.FAN_MODE + ) + + def __init__( + self, device_url: str, coordinator: OverkizDataUpdateCoordinator + ) -> None: + """Init method.""" + super().__init__(device_url, coordinator) + self.temperature_device = self.executor.linked_device( + TEMPERATURE_SENSOR_DEVICE_INDEX + ) + + @property + def current_temperature(self) -> float | None: + """Return the current temperature.""" + if temperature := self.temperature_device.states[OverkizState.CORE_TEMPERATURE]: + return cast(float, temperature.value) + + return None + + async def async_set_hvac_mode(self, hvac_mode: str) -> None: + """Not implemented since there is only one hvac_mode.""" + + @property + def preset_mode(self) -> str | None: + """Return the current preset mode.""" + ventilation_configuration = self.executor.select_state( + OverkizState.IO_VENTILATION_CONFIGURATION_MODE + ) + + if ventilation_configuration == OverkizCommandParam.COMFORT: + return PRESET_AUTO + + if ventilation_configuration == OverkizCommandParam.STANDARD: + return PRESET_MANUAL + + ventilation_mode = cast( + dict, self.executor.select_state(OverkizState.IO_VENTILATION_MODE) + ) + prog = ventilation_mode.get(OverkizCommandParam.PROG) + + if prog == OverkizCommandParam.ON: + return PRESET_PROG + + return None + + async def async_set_preset_mode(self, preset_mode: str) -> None: + """Set the preset mode of the fan.""" + if preset_mode == PRESET_AUTO: + await self.executor.async_execute_command( + OverkizCommand.SET_VENTILATION_CONFIGURATION_MODE, + OverkizCommandParam.COMFORT, + ) + await self._set_ventilation_mode(prog=OverkizCommandParam.OFF) + + if preset_mode == PRESET_PROG: + await self.executor.async_execute_command( + OverkizCommand.SET_VENTILATION_CONFIGURATION_MODE, + OverkizCommandParam.STANDARD, + ) + await self._set_ventilation_mode(prog=OverkizCommandParam.ON) + + if preset_mode == PRESET_MANUAL: + await self.executor.async_execute_command( + OverkizCommand.SET_VENTILATION_CONFIGURATION_MODE, + OverkizCommandParam.STANDARD, + ) + await self._set_ventilation_mode(prog=OverkizCommandParam.OFF) + + await self.executor.async_execute_command( + OverkizCommand.REFRESH_VENTILATION_STATE, + ) + await self.executor.async_execute_command( + OverkizCommand.REFRESH_VENTILATION_CONFIGURATION_MODE, + ) + + @property + def fan_mode(self) -> str | None: + """Return the fan setting.""" + ventilation_mode = cast( + dict, self.executor.select_state(OverkizState.IO_VENTILATION_MODE) + ) + cooling = ventilation_mode.get(OverkizCommandParam.COOLING) + + if cooling == OverkizCommandParam.ON: + return FAN_BYPASS + + return OVERKIZ_TO_FAN_MODES[ + cast(str, self.executor.select_state(OverkizState.IO_AIR_DEMAND_MODE)) + ] + + async def async_set_fan_mode(self, fan_mode: str) -> None: + """Set new target fan mode.""" + if fan_mode == FAN_BYPASS: + await self.executor.async_execute_command( + OverkizCommand.SET_AIR_DEMAND_MODE, OverkizCommandParam.AUTO + ) + await self._set_ventilation_mode(cooling=OverkizCommandParam.ON) + else: + await self._set_ventilation_mode(cooling=OverkizCommandParam.OFF) + await self.executor.async_execute_command( + OverkizCommand.SET_AIR_DEMAND_MODE, FAN_MODES_TO_OVERKIZ[fan_mode] + ) + + await self.executor.async_execute_command( + OverkizCommand.REFRESH_VENTILATION_STATE, + ) + + async def _set_ventilation_mode( + self, + cooling: str | None = None, + prog: str | None = None, + ) -> None: + """Execute ventilation mode command with all parameters.""" + ventilation_mode = cast( + dict, self.executor.select_state(OverkizState.IO_VENTILATION_MODE) + ) + + if cooling: + ventilation_mode[OverkizCommandParam.COOLING] = cooling + + if prog: + ventilation_mode[OverkizCommandParam.PROG] = prog + + await self.executor.async_execute_command( + OverkizCommand.SET_VENTILATION_MODE, ventilation_mode + ) diff --git a/homeassistant/components/overkiz/climate_entities/atlantic_pass_apc_zone_control.py b/homeassistant/components/overkiz/climate_entities/atlantic_pass_apc_zone_control.py index fc1d909390b..ba95785fbc7 100644 --- a/homeassistant/components/overkiz/climate_entities/atlantic_pass_apc_zone_control.py +++ b/homeassistant/components/overkiz/climate_entities/atlantic_pass_apc_zone_control.py @@ -3,11 +3,11 @@ from typing import cast from pyoverkiz.enums import OverkizCommand, OverkizCommandParam, OverkizState -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import HVACMode -from homeassistant.components.overkiz.entity import OverkizEntity +from homeassistant.components.climate import ClimateEntity, HVACMode from homeassistant.const import TEMP_CELSIUS +from ..entity import OverkizEntity + OVERKIZ_TO_HVAC_MODE: dict[str, str] = { OverkizCommandParam.HEATING: HVACMode.HEAT, OverkizCommandParam.DRYING: HVACMode.DRY, diff --git a/homeassistant/components/overkiz/climate_entities/somfy_thermostat.py b/homeassistant/components/overkiz/climate_entities/somfy_thermostat.py index 80859d7561b..608b26b8c9d 100644 --- a/homeassistant/components/overkiz/climate_entities/somfy_thermostat.py +++ b/homeassistant/components/overkiz/climate_entities/somfy_thermostat.py @@ -5,11 +5,11 @@ from typing import Any, cast from pyoverkiz.enums import OverkizCommand, OverkizCommandParam, OverkizState -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( PRESET_AWAY, PRESET_HOME, PRESET_NONE, + ClimateEntity, ClimateEntityFeature, HVACMode, ) diff --git a/homeassistant/components/overkiz/const.py b/homeassistant/components/overkiz/const.py index 9091cd35998..d98709ba2b6 100644 --- a/homeassistant/components/overkiz/const.py +++ b/homeassistant/components/overkiz/const.py @@ -63,6 +63,7 @@ OVERKIZ_DEVICE_TO_PLATFORM: dict[UIClass | UIWidget, Platform | None] = { UIWidget.ALARM_PANEL_CONTROLLER: Platform.ALARM_CONTROL_PANEL, # widgetName, uiClass is Alarm (not supported) UIWidget.ATLANTIC_ELECTRICAL_HEATER: Platform.CLIMATE, # widgetName, uiClass is HeatingSystem (not supported) UIWidget.ATLANTIC_ELECTRICAL_TOWEL_DRYER: Platform.CLIMATE, # widgetName, uiClass is HeatingSystem (not supported) + UIWidget.ATLANTIC_HEAT_RECOVERY_VENTILATION: Platform.CLIMATE, # widgetName, uiClass is HeatingSystem (not supported) UIWidget.ATLANTIC_PASS_APC_ZONE_CONTROL: Platform.CLIMATE, # widgetName, uiClass is HeatingSystem (not supported) UIWidget.DOMESTIC_HOT_WATER_TANK: Platform.SWITCH, # widgetName, uiClass is WaterHeatingSystem (not supported) UIWidget.MY_FOX_ALARM_CONTROLLER: Platform.ALARM_CONTROL_PANEL, # widgetName, uiClass is Alarm (not supported) diff --git a/homeassistant/components/overkiz/cover_entities/vertical_cover.py b/homeassistant/components/overkiz/cover_entities/vertical_cover.py index f76f3849e83..90ac6428960 100644 --- a/homeassistant/components/overkiz/cover_entities/vertical_cover.py +++ b/homeassistant/components/overkiz/cover_entities/vertical_cover.py @@ -152,7 +152,7 @@ class LowSpeedCover(VerticalCover): ) -> None: """Initialize the device.""" super().__init__(device_url, coordinator) - self._attr_name = f"{self._attr_name} Low Speed" + self._attr_name = "Low speed" self._attr_unique_id = f"{self._attr_unique_id}_low_speed" async def async_set_cover_position(self, **kwargs: Any) -> None: diff --git a/homeassistant/components/overkiz/entity.py b/homeassistant/components/overkiz/entity.py index 5728349c5d0..c17f30393fc 100644 --- a/homeassistant/components/overkiz/entity.py +++ b/homeassistant/components/overkiz/entity.py @@ -19,6 +19,8 @@ from .executor import OverkizExecutor class OverkizEntity(CoordinatorEntity[OverkizDataUpdateCoordinator]): """Representation of an Overkiz device entity.""" + _attr_has_entity_name = True + def __init__( self, device_url: str, coordinator: OverkizDataUpdateCoordinator ) -> None: @@ -31,10 +33,18 @@ class OverkizEntity(CoordinatorEntity[OverkizDataUpdateCoordinator]): self._attr_assumed_state = not self.device.states self._attr_available = self.device.available self._attr_unique_id = self.device.device_url - self._attr_name = self.device.label + + if self.is_sub_device: + # In case of sub entity, use the provided label as name + self._attr_name = self.device.label self._attr_device_info = self.generate_device_info() + @property + def is_sub_device(self) -> bool: + """Return True if device is a sub device.""" + return "#" in self.device_url and not self.device_url.endswith("#1") + @property def device(self) -> Device: """Return Overkiz device linked to this entity.""" @@ -45,7 +55,7 @@ class OverkizEntity(CoordinatorEntity[OverkizDataUpdateCoordinator]): # Some devices, such as the Smart Thermostat have several devices in one physical device, # with same device url, terminated by '#' and a number. # In this case, we use the base device url as the device identifier. - if "#" in self.device_url and not self.device_url.endswith("#1"): + if self.is_sub_device: # Only return the url of the base device, to inherit device name and model from parent device. return { "identifiers": {(DOMAIN, self.executor.base_device_url)}, @@ -102,8 +112,9 @@ class OverkizDescriptiveEntity(OverkizEntity): self.entity_description = description self._attr_unique_id = f"{super().unique_id}-{self.entity_description.key}" - if self.entity_description.name: - self._attr_name = f"{super().name} {self.entity_description.name}" + if self.is_sub_device: + # In case of sub device, use the provided label and append the name of the type of entity + self._attr_name = f"{self.device.label} {description.name}" # Used by state translations for sensor and select entities diff --git a/homeassistant/components/overkiz/manifest.json b/homeassistant/components/overkiz/manifest.json index f2d17bab3f9..0b3e041f302 100644 --- a/homeassistant/components/overkiz/manifest.json +++ b/homeassistant/components/overkiz/manifest.json @@ -3,7 +3,7 @@ "name": "Overkiz", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/overkiz", - "requirements": ["pyoverkiz==1.5.0"], + "requirements": ["pyoverkiz==1.5.3"], "zeroconf": [ { "type": "_kizbox._tcp.local.", diff --git a/homeassistant/components/overkiz/number.py b/homeassistant/components/overkiz/number.py index 8e7d2a93ee9..3b4107256a5 100644 --- a/homeassistant/components/overkiz/number.py +++ b/homeassistant/components/overkiz/number.py @@ -1,10 +1,12 @@ """Support for Overkiz (virtual) numbers.""" from __future__ import annotations +import asyncio +from collections.abc import Awaitable, Callable from dataclasses import dataclass from typing import cast -from pyoverkiz.enums import OverkizCommand, OverkizState +from pyoverkiz.enums import OverkizCommand, OverkizCommandParam, OverkizState from homeassistant.components.number import ( NumberDeviceClass, @@ -21,6 +23,9 @@ from . import HomeAssistantOverkizData from .const import DOMAIN, IGNORED_OVERKIZ_DEVICES from .entity import OverkizDescriptiveEntity +BOOST_MODE_DURATION_DELAY = 1 +OPERATING_MODE_DELAY = 3 + @dataclass class OverkizNumberDescriptionMixin: @@ -34,13 +39,48 @@ class OverkizNumberDescription(NumberEntityDescription, OverkizNumberDescription """Class to describe an Overkiz number.""" inverted: bool = False + set_native_value: Callable[ + [float, Callable[..., Awaitable[None]]], Awaitable[None] + ] | None = None + + +async def _async_set_native_value_boost_mode_duration( + value: float, execute_command: Callable[..., Awaitable[None]] +) -> None: + """Update the boost duration value.""" + + if value > 0: + await execute_command(OverkizCommand.SET_BOOST_MODE_DURATION, value) + await asyncio.sleep( + BOOST_MODE_DURATION_DELAY + ) # wait one second to not overload the device + await execute_command( + OverkizCommand.SET_CURRENT_OPERATING_MODE, + { + OverkizCommandParam.RELAUNCH: OverkizCommandParam.ON, + OverkizCommandParam.ABSENCE: OverkizCommandParam.OFF, + }, + ) + else: + await execute_command( + OverkizCommand.SET_CURRENT_OPERATING_MODE, + { + OverkizCommandParam.RELAUNCH: OverkizCommandParam.OFF, + OverkizCommandParam.ABSENCE: OverkizCommandParam.OFF, + }, + ) + + await asyncio.sleep( + OPERATING_MODE_DELAY + ) # wait 3 seconds to have the new duration in + await execute_command(OverkizCommand.REFRESH_BOOST_MODE_DURATION) NUMBER_DESCRIPTIONS: list[OverkizNumberDescription] = [ # Cover: My Position (0 - 100) OverkizNumberDescription( key=OverkizState.CORE_MEMORIZED_1_POSITION, - name="My Position", + name="My position", icon="mdi:content-save-cog", command=OverkizCommand.SET_MEMORIZED_1_POSITION, native_min_value=0, @@ -50,7 +90,7 @@ NUMBER_DESCRIPTIONS: list[OverkizNumberDescription] = [ # WaterHeater: Expected Number Of Shower (2 - 4) OverkizNumberDescription( key=OverkizState.CORE_EXPECTED_NUMBER_OF_SHOWER, - name="Expected Number Of Shower", + name="Expected number of shower", icon="mdi:shower-head", command=OverkizCommand.SET_EXPECTED_NUMBER_OF_SHOWER, native_min_value=2, @@ -60,7 +100,7 @@ NUMBER_DESCRIPTIONS: list[OverkizNumberDescription] = [ # SomfyHeatingTemperatureInterface OverkizNumberDescription( key=OverkizState.CORE_ECO_ROOM_TEMPERATURE, - name="Eco Room Temperature", + name="Eco room temperature", icon="mdi:thermometer", command=OverkizCommand.SET_ECO_TEMPERATURE, device_class=NumberDeviceClass.TEMPERATURE, @@ -71,7 +111,7 @@ NUMBER_DESCRIPTIONS: list[OverkizNumberDescription] = [ ), OverkizNumberDescription( key=OverkizState.CORE_COMFORT_ROOM_TEMPERATURE, - name="Comfort Room Temperature", + name="Comfort room temperature", icon="mdi:home-thermometer-outline", command=OverkizCommand.SET_COMFORT_TEMPERATURE, device_class=NumberDeviceClass.TEMPERATURE, @@ -82,7 +122,7 @@ NUMBER_DESCRIPTIONS: list[OverkizNumberDescription] = [ ), OverkizNumberDescription( key=OverkizState.CORE_SECURED_POSITION_TEMPERATURE, - name="Freeze Protection Temperature", + name="Freeze protection temperature", icon="mdi:sun-thermometer-outline", command=OverkizCommand.SET_SECURED_POSITION_TEMPERATURE, device_class=NumberDeviceClass.TEMPERATURE, @@ -101,6 +141,27 @@ NUMBER_DESCRIPTIONS: list[OverkizNumberDescription] = [ native_max_value=100, inverted=True, ), + # DomesticHotWaterProduction - boost mode duration in days (0 - 7) + OverkizNumberDescription( + key=OverkizState.CORE_BOOST_MODE_DURATION, + name="Boost mode duration", + icon="mdi:water-boiler", + command=OverkizCommand.SET_BOOST_MODE_DURATION, + native_min_value=0, + native_max_value=7, + set_native_value=_async_set_native_value_boost_mode_duration, + entity_category=EntityCategory.CONFIG, + ), + # DomesticHotWaterProduction - away mode in days (0 - 6) + OverkizNumberDescription( + key=OverkizState.IO_AWAY_MODE_DURATION, + name="Away mode duration", + icon="mdi:water-boiler-off", + command=OverkizCommand.SET_AWAY_MODE_DURATION, + native_min_value=0, + native_max_value=6, + entity_category=EntityCategory.CONFIG, + ), ] SUPPORTED_STATES = {description.key: description for description in NUMBER_DESCRIPTIONS} @@ -156,6 +217,12 @@ class OverkizNumber(OverkizDescriptiveEntity, NumberEntity): if self.entity_description.inverted: value = self.native_max_value - value + if self.entity_description.set_native_value: + await self.entity_description.set_native_value( + value, self.executor.async_execute_command + ) + return + await self.executor.async_execute_command( self.entity_description.command, value ) diff --git a/homeassistant/components/overkiz/select.py b/homeassistant/components/overkiz/select.py index 8482932e2e4..3b40eccfbf6 100644 --- a/homeassistant/components/overkiz/select.py +++ b/homeassistant/components/overkiz/select.py @@ -76,7 +76,7 @@ SELECT_DESCRIPTIONS: list[OverkizSelectDescription] = [ ), OverkizSelectDescription( key=OverkizState.IO_MEMORIZED_SIMPLE_VOLUME, - name="Memorized Simple Volume", + name="Memorized simple volume", icon="mdi:volume-high", options=[OverkizCommandParam.STANDARD, OverkizCommandParam.HIGHEST], select_option=_select_option_memorized_simple_volume, @@ -86,7 +86,7 @@ SELECT_DESCRIPTIONS: list[OverkizSelectDescription] = [ # SomfyHeatingTemperatureInterface OverkizSelectDescription( key=OverkizState.OVP_HEATING_TEMPERATURE_INTERFACE_OPERATING_MODE, - name="Operating Mode", + name="Operating mode", icon="mdi:sun-snowflake", options=[OverkizCommandParam.HEATING, OverkizCommandParam.COOLING], select_option=lambda option, execute_command: execute_command( @@ -97,7 +97,7 @@ SELECT_DESCRIPTIONS: list[OverkizSelectDescription] = [ # StatefulAlarmController OverkizSelectDescription( key=OverkizState.CORE_ACTIVE_ZONES, - name="Active Zones", + name="Active zones", icon="mdi:shield-lock", options=["", "A", "B", "C", "A,B", "B,C", "A,C", "A,B,C"], select_option=_select_option_active_zone, diff --git a/homeassistant/components/overkiz/sensor.py b/homeassistant/components/overkiz/sensor.py index ac32c76c459..e80e08e263a 100644 --- a/homeassistant/components/overkiz/sensor.py +++ b/homeassistant/components/overkiz/sensor.py @@ -48,7 +48,7 @@ class OverkizSensorDescription(SensorEntityDescription): SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [ OverkizSensorDescription( key=OverkizState.CORE_BATTERY_LEVEL, - name="Battery Level", + name="Battery level", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.BATTERY, state_class=SensorStateClass.MEASUREMENT, @@ -64,7 +64,7 @@ SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [ ), OverkizSensorDescription( key=OverkizState.CORE_RSSI_LEVEL, - name="RSSI Level", + name="RSSI level", native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS, device_class=SensorDeviceClass.SIGNAL_STRENGTH, state_class=SensorStateClass.MEASUREMENT, @@ -74,71 +74,74 @@ SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [ ), OverkizSensorDescription( key=OverkizState.CORE_EXPECTED_NUMBER_OF_SHOWER, - name="Expected Number Of Shower", + name="Expected number of shower", icon="mdi:shower-head", state_class=SensorStateClass.MEASUREMENT, ), OverkizSensorDescription( key=OverkizState.CORE_NUMBER_OF_SHOWER_REMAINING, - name="Number of Shower Remaining", + name="Number of shower remaining", icon="mdi:shower-head", state_class=SensorStateClass.MEASUREMENT, ), # V40 is measured in litres (L) and shows the amount of warm (mixed) water with a temperature of 40 C, which can be drained from a switched off electric water heater. OverkizSensorDescription( key=OverkizState.CORE_V40_WATER_VOLUME_ESTIMATION, - name="Water Volume Estimation at 40 °C", + name="Water volume estimation at 40 °C", icon="mdi:water", native_unit_of_measurement=VOLUME_LITERS, + device_class=SensorDeviceClass.VOLUME, entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, ), OverkizSensorDescription( key=OverkizState.CORE_WATER_CONSUMPTION, - name="Water Consumption", + name="Water consumption", icon="mdi:water", native_unit_of_measurement=VOLUME_LITERS, + device_class=SensorDeviceClass.VOLUME, state_class=SensorStateClass.MEASUREMENT, ), OverkizSensorDescription( key=OverkizState.IO_OUTLET_ENGINE, - name="Outlet Engine", + name="Outlet engine", icon="mdi:fan-chevron-down", native_unit_of_measurement=VOLUME_LITERS, + device_class=SensorDeviceClass.VOLUME, state_class=SensorStateClass.MEASUREMENT, ), OverkizSensorDescription( key=OverkizState.IO_INLET_ENGINE, - name="Inlet Engine", + name="Inlet engine", icon="mdi:fan-chevron-up", native_unit_of_measurement=VOLUME_FLOW_RATE_CUBIC_METERS_PER_HOUR, state_class=SensorStateClass.MEASUREMENT, ), OverkizSensorDescription( key=OverkizState.HLRRWIFI_ROOM_TEMPERATURE, - name="Room Temperature", + name="Room temperature", device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=TEMP_CELSIUS, ), OverkizSensorDescription( key=OverkizState.IO_MIDDLE_WATER_TEMPERATURE, - name="Middle Water Temperature", + name="Middle water temperature", device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=TEMP_CELSIUS, ), OverkizSensorDescription( key=OverkizState.CORE_FOSSIL_ENERGY_CONSUMPTION, - name="Fossil Energy Consumption", + name="Fossil energy consumption", ), OverkizSensorDescription( key=OverkizState.CORE_GAS_CONSUMPTION, - name="Gas Consumption", + name="Gas consumption", ), OverkizSensorDescription( key=OverkizState.CORE_THERMAL_ENERGY_CONSUMPTION, - name="Thermal Energy Consumption", + name="Thermal energy consumption", ), # LightSensor/LuminanceSensor OverkizSensorDescription( @@ -151,21 +154,21 @@ SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [ # ElectricitySensor/CumulativeElectricPowerConsumptionSensor OverkizSensorDescription( key=OverkizState.CORE_ELECTRIC_ENERGY_CONSUMPTION, - name="Electric Energy Consumption", + name="Electric energy consumption", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_WATT_HOUR, # core:MeasuredValueType = core:ElectricalEnergyInWh (not for modbus:YutakiV2DHWElectricalEnergyConsumptionComponent) state_class=SensorStateClass.TOTAL_INCREASING, # core:MeasurementCategory attribute = electric/overall ), OverkizSensorDescription( key=OverkizState.CORE_ELECTRIC_POWER_CONSUMPTION, - name="Electric Power Consumption", + name="Electric power consumption", device_class=SensorDeviceClass.POWER, native_unit_of_measurement=POWER_WATT, # core:MeasuredValueType = core:ElectricalEnergyInWh (not for modbus:YutakiV2DHWElectricalEnergyConsumptionComponent) state_class=SensorStateClass.MEASUREMENT, ), OverkizSensorDescription( key=OverkizState.CORE_CONSUMPTION_TARIFF1, - name="Consumption Tariff 1", + name="Consumption tariff 1", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_WATT_HOUR, # core:MeasuredValueType = core:ElectricalEnergyInWh entity_registry_enabled_default=False, @@ -173,7 +176,7 @@ SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [ ), OverkizSensorDescription( key=OverkizState.CORE_CONSUMPTION_TARIFF2, - name="Consumption Tariff 2", + name="Consumption tariff 2", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_WATT_HOUR, # core:MeasuredValueType = core:ElectricalEnergyInWh entity_registry_enabled_default=False, @@ -181,7 +184,7 @@ SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [ ), OverkizSensorDescription( key=OverkizState.CORE_CONSUMPTION_TARIFF3, - name="Consumption Tariff 3", + name="Consumption tariff 3", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_WATT_HOUR, # core:MeasuredValueType = core:ElectricalEnergyInWh entity_registry_enabled_default=False, @@ -189,7 +192,7 @@ SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [ ), OverkizSensorDescription( key=OverkizState.CORE_CONSUMPTION_TARIFF4, - name="Consumption Tariff 4", + name="Consumption tariff 4", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_WATT_HOUR, # core:MeasuredValueType = core:ElectricalEnergyInWh entity_registry_enabled_default=False, @@ -197,7 +200,7 @@ SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [ ), OverkizSensorDescription( key=OverkizState.CORE_CONSUMPTION_TARIFF5, - name="Consumption Tariff 5", + name="Consumption tariff 5", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_WATT_HOUR, # core:MeasuredValueType = core:ElectricalEnergyInWh entity_registry_enabled_default=False, @@ -205,7 +208,7 @@ SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [ ), OverkizSensorDescription( key=OverkizState.CORE_CONSUMPTION_TARIFF6, - name="Consumption Tariff 6", + name="Consumption tariff 6", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_WATT_HOUR, # core:MeasuredValueType = core:ElectricalEnergyInWh entity_registry_enabled_default=False, @@ -213,7 +216,7 @@ SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [ ), OverkizSensorDescription( key=OverkizState.CORE_CONSUMPTION_TARIFF7, - name="Consumption Tariff 7", + name="Consumption tariff 7", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_WATT_HOUR, # core:MeasuredValueType = core:ElectricalEnergyInWh entity_registry_enabled_default=False, @@ -221,7 +224,7 @@ SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [ ), OverkizSensorDescription( key=OverkizState.CORE_CONSUMPTION_TARIFF8, - name="Consumption Tariff 8", + name="Consumption tariff 8", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_WATT_HOUR, # core:MeasuredValueType = core:ElectricalEnergyInWh entity_registry_enabled_default=False, @@ -229,7 +232,7 @@ SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [ ), OverkizSensorDescription( key=OverkizState.CORE_CONSUMPTION_TARIFF9, - name="Consumption Tariff 9", + name="Consumption tariff 9", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_WATT_HOUR, # core:MeasuredValueType = core:ElectricalEnergyInWh entity_registry_enabled_default=False, @@ -238,7 +241,7 @@ SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [ # HumiditySensor/RelativeHumiditySensor OverkizSensorDescription( key=OverkizState.CORE_RELATIVE_HUMIDITY, - name="Relative Humidity", + name="Relative humidity", native_value=lambda value: round(cast(float, value), 2), device_class=SensorDeviceClass.HUMIDITY, native_unit_of_measurement=PERCENTAGE, # core:MeasuredValueType = core:RelativeValueInPercentage @@ -256,21 +259,21 @@ SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [ # WeatherSensor/WeatherForecastSensor OverkizSensorDescription( key=OverkizState.CORE_WEATHER_STATUS, - name="Weather Status", + name="Weather status", device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=TEMP_CELSIUS, state_class=SensorStateClass.MEASUREMENT, ), OverkizSensorDescription( key=OverkizState.CORE_MINIMUM_TEMPERATURE, - name="Minimum Temperature", + name="Minimum temperature", device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=TEMP_CELSIUS, state_class=SensorStateClass.MEASUREMENT, ), OverkizSensorDescription( key=OverkizState.CORE_MAXIMUM_TEMPERATURE, - name="Maximum Temperature", + name="Maximum temperature", device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=TEMP_CELSIUS, state_class=SensorStateClass.MEASUREMENT, @@ -278,7 +281,7 @@ SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [ # AirSensor/COSensor OverkizSensorDescription( key=OverkizState.CORE_CO_CONCENTRATION, - name="CO Concentration", + name="CO concentration", device_class=SensorDeviceClass.CO, native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, state_class=SensorStateClass.MEASUREMENT, @@ -286,7 +289,7 @@ SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [ # AirSensor/CO2Sensor OverkizSensorDescription( key=OverkizState.CORE_CO2_CONCENTRATION, - name="CO2 Concentration", + name="CO2 concentration", device_class=SensorDeviceClass.CO2, native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, state_class=SensorStateClass.MEASUREMENT, @@ -294,7 +297,7 @@ SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [ # SunSensor/SunEnergySensor OverkizSensorDescription( key=OverkizState.CORE_SUN_ENERGY, - name="Sun Energy", + name="Sun energy", native_value=lambda value: round(cast(float, value), 2), icon="mdi:solar-power", state_class=SensorStateClass.MEASUREMENT, @@ -302,7 +305,7 @@ SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [ # WindSensor/WindSpeedSensor OverkizSensorDescription( key=OverkizState.CORE_WIND_SPEED, - name="Wind Speed", + name="Wind speed", native_value=lambda value: round(cast(float, value), 2), icon="mdi:weather-windy", state_class=SensorStateClass.MEASUREMENT, @@ -310,14 +313,14 @@ SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [ # SmokeSensor/SmokeSensor OverkizSensorDescription( key=OverkizState.IO_SENSOR_ROOM, - name="Sensor Room", + name="Sensor room", device_class=OverkizDeviceClass.SENSOR_ROOM, entity_category=EntityCategory.DIAGNOSTIC, icon="mdi:spray-bottle", ), OverkizSensorDescription( key=OverkizState.IO_PRIORITY_LOCK_ORIGINATOR, - name="Priority Lock Originator", + name="Priority lock originator", device_class=OverkizDeviceClass.PRIORITY_LOCK_ORIGINATOR, icon="mdi:lock", entity_registry_enabled_default=False, @@ -327,14 +330,14 @@ SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [ ), OverkizSensorDescription( key=OverkizState.CORE_PRIORITY_LOCK_TIMER, - name="Priority Lock Timer", + name="Priority lock timer", icon="mdi:lock-clock", native_unit_of_measurement=TIME_SECONDS, entity_registry_enabled_default=False, ), OverkizSensorDescription( key=OverkizState.CORE_DISCRETE_RSSI_LEVEL, - name="Discrete RSSI Level", + name="Discrete RSSI level", entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, device_class=OverkizDeviceClass.DISCRETE_RSSI_LEVEL, @@ -342,7 +345,7 @@ SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [ ), OverkizSensorDescription( key=OverkizState.CORE_SENSOR_DEFECT, - name="Sensor Defect", + name="Sensor defect", entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, device_class=OverkizDeviceClass.SENSOR_DEFECT, @@ -353,23 +356,29 @@ SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [ # DomesticHotWaterProduction/WaterHeatingSystem OverkizSensorDescription( key=OverkizState.IO_HEAT_PUMP_OPERATING_TIME, - name="Heat Pump Operating Time", + name="Heat pump operating time", + device_class=SensorDeviceClass.DURATION, + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=TIME_SECONDS, ), OverkizSensorDescription( key=OverkizState.IO_ELECTRIC_BOOSTER_OPERATING_TIME, - name="Electric Booster Operating Time", + name="Electric booster operating time", + device_class=SensorDeviceClass.DURATION, + native_unit_of_measurement=TIME_SECONDS, + entity_category=EntityCategory.DIAGNOSTIC, ), # Cover OverkizSensorDescription( key=OverkizState.CORE_TARGET_CLOSURE, - name="Target Closure", + name="Target closure", native_unit_of_measurement=PERCENTAGE, entity_registry_enabled_default=False, ), # ThreeWayWindowHandle/WindowHandle OverkizSensorDescription( key=OverkizState.CORE_THREE_WAY_HANDLE_DIRECTION, - name="Three Way Handle Direction", + name="Three way handle direction", device_class=OverkizDeviceClass.THREE_WAY_HANDLE_DIRECTION, ), ] @@ -448,7 +457,7 @@ class OverkizHomeKitSetupCodeSensor(OverkizEntity, SensorEntity): ) -> None: """Initialize the device.""" super().__init__(device_url, coordinator) - self._attr_name = "HomeKit Setup Code" + self._attr_name = "HomeKit setup code" @property def native_value(self) -> str | None: diff --git a/homeassistant/components/overkiz/siren.py b/homeassistant/components/overkiz/siren.py index 02736f6f50a..f60eb04cfd3 100644 --- a/homeassistant/components/overkiz/siren.py +++ b/homeassistant/components/overkiz/siren.py @@ -4,8 +4,11 @@ from typing import Any from pyoverkiz.enums import OverkizState from pyoverkiz.enums.command import OverkizCommand, OverkizCommandParam -from homeassistant.components.siren import SirenEntity, SirenEntityFeature -from homeassistant.components.siren.const import ATTR_DURATION +from homeassistant.components.siren import ( + ATTR_DURATION, + SirenEntity, + SirenEntityFeature, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant diff --git a/homeassistant/components/overkiz/switch.py b/homeassistant/components/overkiz/switch.py index a198964b5d3..a7dcfe54a2a 100644 --- a/homeassistant/components/overkiz/switch.py +++ b/homeassistant/components/overkiz/switch.py @@ -98,7 +98,7 @@ SWITCH_DESCRIPTIONS: list[OverkizSwitchDescription] = [ ), OverkizSwitchDescription( key=UIWidget.MY_FOX_SECURITY_CAMERA, - name="Camera Shutter", + name="Camera shutter", turn_on=OverkizCommand.OPEN, turn_off=OverkizCommand.CLOSE, icon="mdi:camera-lock", diff --git a/homeassistant/components/overkiz/translations/bg.json b/homeassistant/components/overkiz/translations/bg.json index afee50a6b00..ff6e8f03030 100644 --- a/homeassistant/components/overkiz/translations/bg.json +++ b/homeassistant/components/overkiz/translations/bg.json @@ -10,7 +10,8 @@ "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", "server_in_maintenance": "\u0421\u044a\u0440\u0432\u044a\u0440\u044a\u0442 \u0435 \u0441\u043f\u0440\u044f\u043d \u0437\u0430 \u043f\u043e\u0434\u0434\u0440\u044a\u0436\u043a\u0430", "too_many_requests": "\u0422\u0432\u044a\u0440\u0434\u0435 \u043c\u043d\u043e\u0433\u043e \u0437\u0430\u044f\u0432\u043a\u0438, \u043e\u043f\u0438\u0442\u0430\u0439\u0442\u0435 \u043e\u0442\u043d\u043e\u0432\u043e \u043f\u043e-\u043a\u044a\u0441\u043d\u043e.", - "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430", + "unknown_user": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u0435\u043d \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b. \u0410\u043a\u0430\u0443\u043d\u0442\u0438\u0442\u0435 \u043d\u0430 Somfy Protect \u043d\u0435 \u0441\u0435 \u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430\u0442 \u043e\u0442 \u0442\u0430\u0437\u0438 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f." }, "flow_title": "\u0428\u043b\u044e\u0437: {gateway_id}", "step": { diff --git a/homeassistant/components/overkiz/translations/bn.json b/homeassistant/components/overkiz/translations/bn.json new file mode 100644 index 00000000000..de652521c3c --- /dev/null +++ b/homeassistant/components/overkiz/translations/bn.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "unknown_user": "\u0985\u09aa\u09b0\u09bf\u099a\u09bf\u09a4 \u09ac\u09cd\u09af\u09ac\u09b9\u09be\u09b0\u0995\u09be\u09b0\u09c0\u0964 Somfy Protect \u0985\u09cd\u09af\u09be\u0995\u09be\u0989\u09a8\u09cd\u099f\u0997\u09c1\u09b2\u09bf \u098f\u0987 \u0987\u09a8\u09cd\u099f\u09bf\u0997\u09cd\u09b0\u09c7\u09b6\u09a8 \u09a6\u09cd\u09ac\u09be\u09b0\u09be \u09b8\u09ae\u09b0\u09cd\u09a5\u09bf\u09a4 \u09a8\u09af\u09bc\u0964" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/overkiz/translations/ca.json b/homeassistant/components/overkiz/translations/ca.json index d3a5a38edc3..ca55a8468b3 100644 --- a/homeassistant/components/overkiz/translations/ca.json +++ b/homeassistant/components/overkiz/translations/ca.json @@ -11,7 +11,8 @@ "server_in_maintenance": "El servidor est\u00e0 inoperatiu per manteniment", "too_many_attempts": "Massa intents amb un 'token' inv\u00e0lid, bloquejat temporalment", "too_many_requests": "Massa sol\u00b7licituds, torna-ho a provar m\u00e9s tard", - "unknown": "Error inesperat" + "unknown": "Error inesperat", + "unknown_user": "Usuari desconegut. Els comptes de Somfy Protect no s\u00f3n compatibles amb aquesta integraci\u00f3." }, "flow_title": "Passarel\u00b7la: {gateway_id}", "step": { diff --git a/homeassistant/components/overkiz/translations/de.json b/homeassistant/components/overkiz/translations/de.json index 09cce8ea63f..1e4cd0cb254 100644 --- a/homeassistant/components/overkiz/translations/de.json +++ b/homeassistant/components/overkiz/translations/de.json @@ -11,7 +11,8 @@ "server_in_maintenance": "Server ist wegen Wartungsarbeiten au\u00dfer Betrieb", "too_many_attempts": "Zu viele Versuche mit einem ung\u00fcltigen Token, vor\u00fcbergehend gesperrt", "too_many_requests": "Zu viele Anfragen, versuche es sp\u00e4ter erneut.", - "unknown": "Unerwarteter Fehler" + "unknown": "Unerwarteter Fehler", + "unknown_user": "Unbekannter Benutzer. Somfy Protect-Konten werden von dieser Integration nicht unterst\u00fctzt." }, "flow_title": "Gateway: {gateway_id}", "step": { diff --git a/homeassistant/components/overkiz/translations/el.json b/homeassistant/components/overkiz/translations/el.json index 924ecc3219d..e9862479c27 100644 --- a/homeassistant/components/overkiz/translations/el.json +++ b/homeassistant/components/overkiz/translations/el.json @@ -11,7 +11,8 @@ "server_in_maintenance": "\u039f \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03ba\u03c4\u03cc\u03c2 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1\u03c2 \u03b3\u03b9\u03b1 \u03c3\u03c5\u03bd\u03c4\u03ae\u03c1\u03b7\u03c3\u03b7", "too_many_attempts": "\u03a0\u03ac\u03c1\u03b1 \u03c0\u03bf\u03bb\u03bb\u03ad\u03c2 \u03c0\u03c1\u03bf\u03c3\u03c0\u03ac\u03b8\u03b5\u03b9\u03b5\u03c2 \u03bc\u03b5 \u03bc\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc, \u03c0\u03c1\u03bf\u03c3\u03c9\u03c1\u03b9\u03bd\u03ac \u03b1\u03c0\u03bf\u03ba\u03bb\u03b5\u03b9\u03c3\u03bc\u03ad\u03bd\u03b5\u03c2", "too_many_requests": "\u03a0\u03ac\u03c1\u03b1 \u03c0\u03bf\u03bb\u03bb\u03ac \u03b1\u03b9\u03c4\u03ae\u03bc\u03b1\u03c4\u03b1, \u03c0\u03c1\u03bf\u03c3\u03c0\u03b1\u03b8\u03ae\u03c3\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac \u03b1\u03c1\u03b3\u03cc\u03c4\u03b5\u03c1\u03b1", - "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1", + "unknown_user": "\u0386\u03b3\u03bd\u03c9\u03c3\u03c4\u03bf\u03c2 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7\u03c2. \u039f\u03b9 \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03bf\u03af Somfy Protect \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03bf\u03bd\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03b1\u03c5\u03c4\u03ae \u03c4\u03b7\u03bd \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7." }, "flow_title": "\u03a0\u03cd\u03bb\u03b7: {gateway_id}", "step": { diff --git a/homeassistant/components/overkiz/translations/es.json b/homeassistant/components/overkiz/translations/es.json index a2b04f05cf9..a5a3ad16e0d 100644 --- a/homeassistant/components/overkiz/translations/es.json +++ b/homeassistant/components/overkiz/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente", "reauth_wrong_account": "Solo puedes volver a autenticar esta entrada con la misma cuenta y concentrador de Overkiz" }, "error": { @@ -11,7 +11,8 @@ "server_in_maintenance": "El servidor est\u00e1 ca\u00eddo por mantenimiento", "too_many_attempts": "Demasiados intentos con un token no v\u00e1lido, prohibido temporalmente", "too_many_requests": "Demasiadas solicitudes, vuelve a intentarlo m\u00e1s tarde", - "unknown": "Error inesperado" + "unknown": "Error inesperado", + "unknown_user": "Usuario desconocido. Las cuentas de Somfy Protect no son compatibles con esta integraci\u00f3n." }, "flow_title": "Puerta de enlace: {gateway_id}", "step": { diff --git a/homeassistant/components/overkiz/translations/et.json b/homeassistant/components/overkiz/translations/et.json index 3cbfcb6af80..34639ea1739 100644 --- a/homeassistant/components/overkiz/translations/et.json +++ b/homeassistant/components/overkiz/translations/et.json @@ -11,7 +11,8 @@ "server_in_maintenance": "Server on hoolduse t\u00f5ttu maas", "too_many_attempts": "Liiga palju katseid kehtetu v\u00f5tmega, ajutiselt keelatud", "too_many_requests": "Liiga palju p\u00e4ringuid, proovi hiljem uuesti", - "unknown": "Ootamatu t\u00f5rge" + "unknown": "Ootamatu t\u00f5rge", + "unknown_user": "Tundmatu kasutaja. See sidumine ei toeta Somfy Protecti kontosid." }, "flow_title": "L\u00fc\u00fcs: {gateway_id}", "step": { diff --git a/homeassistant/components/overkiz/translations/fr.json b/homeassistant/components/overkiz/translations/fr.json index c919301d541..89d7af10f33 100644 --- a/homeassistant/components/overkiz/translations/fr.json +++ b/homeassistant/components/overkiz/translations/fr.json @@ -11,7 +11,8 @@ "server_in_maintenance": "Le serveur est ferm\u00e9 pour maintenance", "too_many_attempts": "Trop de tentatives avec un jeton non valide\u00a0: banni temporairement", "too_many_requests": "Trop de demandes, r\u00e9essayez plus tard.", - "unknown": "Erreur inattendue" + "unknown": "Erreur inattendue", + "unknown_user": "Utilisateur inconnu. Les comptes Somfy Protect ne sont pas pris en charge par cette int\u00e9gration." }, "flow_title": "Passerelle\u00a0: {gateway_id}", "step": { diff --git a/homeassistant/components/overkiz/translations/hu.json b/homeassistant/components/overkiz/translations/hu.json index b6810749bd3..4fa4c0a9ddc 100644 --- a/homeassistant/components/overkiz/translations/hu.json +++ b/homeassistant/components/overkiz/translations/hu.json @@ -11,7 +11,8 @@ "server_in_maintenance": "A szerver karbantart\u00e1s miatt nem el\u00e9rhet\u0151", "too_many_attempts": "T\u00fal sok pr\u00f3b\u00e1lkoz\u00e1s \u00e9rv\u00e9nytelen tokennel, ideiglenesen kitiltva", "too_many_requests": "T\u00fal sok a k\u00e9r\u00e9s, pr\u00f3b\u00e1lja meg k\u00e9s\u0151bb \u00fajra.", - "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt", + "unknown_user": "Ismeretlen felhaszn\u00e1l\u00f3. Ez az integr\u00e1ci\u00f3 nem t\u00e1mogatja a Somfy Protect fi\u00f3kokat." }, "flow_title": "\u00c1tj\u00e1r\u00f3: {gateway_id}", "step": { diff --git a/homeassistant/components/overkiz/translations/id.json b/homeassistant/components/overkiz/translations/id.json index c58b66b10a7..709cef9819c 100644 --- a/homeassistant/components/overkiz/translations/id.json +++ b/homeassistant/components/overkiz/translations/id.json @@ -11,7 +11,8 @@ "server_in_maintenance": "Server sedang dalam masa pemeliharaan", "too_many_attempts": "Terlalu banyak percobaan dengan token yang tidak valid, untuk sementara diblokir", "too_many_requests": "Terlalu banyak permintaan, coba lagi nanti.", - "unknown": "Kesalahan yang tidak diharapkan" + "unknown": "Kesalahan yang tidak diharapkan", + "unknown_user": "Pengguna tidak dikenal. Akun Somfy Protect tidak didukung oleh integrasi ini." }, "flow_title": "Gateway: {gateway_id}", "step": { diff --git a/homeassistant/components/overkiz/translations/it.json b/homeassistant/components/overkiz/translations/it.json index 3dcc4e94e10..21602a4cb89 100644 --- a/homeassistant/components/overkiz/translations/it.json +++ b/homeassistant/components/overkiz/translations/it.json @@ -11,7 +11,8 @@ "server_in_maintenance": "Il server \u00e8 inattivo per manutenzione", "too_many_attempts": "Troppi tentativi con un token non valido, temporaneamente bandito", "too_many_requests": "Troppe richieste, riprova pi\u00f9 tardi.", - "unknown": "Errore imprevisto" + "unknown": "Errore imprevisto", + "unknown_user": "Utente sconosciuto. Gli account Somfy Protect non sono supportati da questa integrazione." }, "flow_title": "Gateway: {gateway_id}", "step": { diff --git a/homeassistant/components/overkiz/translations/ja.json b/homeassistant/components/overkiz/translations/ja.json index 357408847fd..d2f72355dfd 100644 --- a/homeassistant/components/overkiz/translations/ja.json +++ b/homeassistant/components/overkiz/translations/ja.json @@ -10,8 +10,9 @@ "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "server_in_maintenance": "\u30e1\u30f3\u30c6\u30ca\u30f3\u30b9\u306e\u305f\u3081\u30b5\u30fc\u30d0\u30fc\u304c\u30c0\u30a6\u30f3\u3057\u3066\u3044\u307e\u3059", "too_many_attempts": "\u7121\u52b9\u306a\u30c8\u30fc\u30af\u30f3\u306b\u3088\u308b\u8a66\u884c\u56de\u6570\u304c\u591a\u3059\u304e\u305f\u305f\u3081\u3001\u4e00\u6642\u7684\u306b\u7981\u6b62\u3055\u308c\u307e\u3057\u305f\u3002", - "too_many_requests": "\u30ea\u30af\u30a8\u30b9\u30c8\u304c\u591a\u3059\u304e\u307e\u3059\u3002\u3057\u3070\u3089\u304f\u3057\u3066\u304b\u3089\u3082\u3046\u4e00\u5ea6\u304a\u8a66\u3057\u304f\u3060\u3055\u3044\u3002", - "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + "too_many_requests": "\u30ea\u30af\u30a8\u30b9\u30c8\u304c\u591a\u3059\u304e\u307e\u3059\u3002\u3057\u3070\u3089\u304f\u3057\u3066\u304b\u3089\u3082\u3046\u4e00\u5ea6\u304a\u8a66\u3057\u304f\u3060\u3055\u3044", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc", + "unknown_user": "\u4e0d\u660e\u306a\u30e6\u30fc\u30b6\u30fc\u3067\u3059\u3002Somfy Protect\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3001\u3053\u306e\u7d71\u5408\u3067\u306f\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002" }, "flow_title": "\u30b2\u30fc\u30c8\u30a6\u30a7\u30a4: {gateway_id}", "step": { diff --git a/homeassistant/components/overkiz/translations/no.json b/homeassistant/components/overkiz/translations/no.json index e0d52e0fc0e..2da02db164d 100644 --- a/homeassistant/components/overkiz/translations/no.json +++ b/homeassistant/components/overkiz/translations/no.json @@ -11,7 +11,8 @@ "server_in_maintenance": "serveren er nede for vedlikehold", "too_many_attempts": "For mange fors\u00f8k med et ugyldig token, midlertidig utestengt", "too_many_requests": "For mange foresp\u00f8rsler. Pr\u00f8v igjen senere", - "unknown": "Uventet feil" + "unknown": "Uventet feil", + "unknown_user": "Ukjent bruker. Somfy Protect-kontoer st\u00f8ttes ikke av denne integrasjonen." }, "flow_title": "Gateway: {gateway_id}", "step": { diff --git a/homeassistant/components/overkiz/translations/pl.json b/homeassistant/components/overkiz/translations/pl.json index 7044dc717fd..23065776db4 100644 --- a/homeassistant/components/overkiz/translations/pl.json +++ b/homeassistant/components/overkiz/translations/pl.json @@ -11,7 +11,8 @@ "server_in_maintenance": "Serwer wy\u0142\u0105czony z powodu przerwy technicznej", "too_many_attempts": "Zbyt wiele pr\u00f3b z nieprawid\u0142owym tokenem, konto tymczasowo zablokowane", "too_many_requests": "Zbyt wiele \u017c\u0105da\u0144, spr\u00f3buj ponownie p\u00f3\u017aniej.", - "unknown": "Nieoczekiwany b\u0142\u0105d" + "unknown": "Nieoczekiwany b\u0142\u0105d", + "unknown_user": "Nieznany u\u017cytkownik. Konta Somfy Protect nie s\u0105 obs\u0142ugiwane przez t\u0119 integracj\u0119." }, "flow_title": "Bramka: {gateway_id}", "step": { diff --git a/homeassistant/components/overkiz/translations/pt-BR.json b/homeassistant/components/overkiz/translations/pt-BR.json index 123b2a83d42..3e2ff048560 100644 --- a/homeassistant/components/overkiz/translations/pt-BR.json +++ b/homeassistant/components/overkiz/translations/pt-BR.json @@ -11,7 +11,8 @@ "server_in_maintenance": "O servidor est\u00e1 fora de servi\u00e7o para manuten\u00e7\u00e3o", "too_many_attempts": "Muitas tentativas com um token inv\u00e1lido, banido temporariamente", "too_many_requests": "Muitas solicita\u00e7\u00f5es, tente novamente mais tarde", - "unknown": "Erro inesperado" + "unknown": "Erro inesperado", + "unknown_user": "Usu\u00e1rio desconhecido. As contas Somfy Protect n\u00e3o s\u00e3o suportadas por esta integra\u00e7\u00e3o." }, "flow_title": "Gateway: {gateway_id}", "step": { diff --git a/homeassistant/components/overkiz/translations/pt.json b/homeassistant/components/overkiz/translations/pt.json index 1e3d9138c84..5b2dd940959 100644 --- a/homeassistant/components/overkiz/translations/pt.json +++ b/homeassistant/components/overkiz/translations/pt.json @@ -1,7 +1,8 @@ { "config": { "error": { - "unknown": "Erro inesperado" + "unknown": "Erro inesperado", + "unknown_user": "Usu\u00e1rio desconhecido. As contas Somfy Protect n\u00e3o s\u00e3o suportadas por esta integra\u00e7\u00e3o." }, "step": { "user": { diff --git a/homeassistant/components/overkiz/translations/ru.json b/homeassistant/components/overkiz/translations/ru.json index 53f08e1dbd6..17ef0ecd27e 100644 --- a/homeassistant/components/overkiz/translations/ru.json +++ b/homeassistant/components/overkiz/translations/ru.json @@ -11,7 +11,8 @@ "server_in_maintenance": "\u0421\u0435\u0440\u0432\u0435\u0440 \u043d\u0435 \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u0432 \u0441\u0432\u044f\u0437\u0438 \u0441 \u0442\u0435\u0445\u043d\u0438\u0447\u0435\u0441\u043a\u0438\u043c \u043e\u0431\u0441\u043b\u0443\u0436\u0438\u0432\u0430\u043d\u0438\u0435\u043c.", "too_many_attempts": "\u0421\u043b\u0438\u0448\u043a\u043e\u043c \u043c\u043d\u043e\u0433\u043e \u043f\u043e\u043f\u044b\u0442\u043e\u043a \u0441 \u043d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u043c \u0442\u043e\u043a\u0435\u043d\u043e\u043c, \u0432\u0440\u0435\u043c\u0435\u043d\u043d\u043e \u0437\u0430\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u043d\u043e.", "too_many_requests": "\u0421\u043b\u0438\u0448\u043a\u043e\u043c \u043c\u043d\u043e\u0433\u043e \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432, \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443 \u043f\u043e\u0437\u0436\u0435.", - "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430.", + "unknown_user": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u044b\u0439 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c. \u042d\u0442\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0437\u0430\u043f\u0438\u0441\u0438 Somfy Protect." }, "flow_title": "\u0428\u043b\u044e\u0437: {gateway_id}", "step": { diff --git a/homeassistant/components/overkiz/translations/sensor.pl.json b/homeassistant/components/overkiz/translations/sensor.pl.json index 440cf4998f8..dd826600f49 100644 --- a/homeassistant/components/overkiz/translations/sensor.pl.json +++ b/homeassistant/components/overkiz/translations/sensor.pl.json @@ -16,7 +16,7 @@ "external_gateway": "bramka zewn\u0119trzna", "local_user": "u\u017cytkownik lokalny", "lsc": "LSC", - "myself": "Ja", + "myself": "ja", "rain": "deszcz", "saac": "SAAC", "security": "bezpiecze\u0144stwo", diff --git a/homeassistant/components/overkiz/translations/sv.json b/homeassistant/components/overkiz/translations/sv.json index b173a0bef99..32565cac512 100644 --- a/homeassistant/components/overkiz/translations/sv.json +++ b/homeassistant/components/overkiz/translations/sv.json @@ -11,7 +11,8 @@ "server_in_maintenance": "Servern ligger nere f\u00f6r underh\u00e5ll", "too_many_attempts": "F\u00f6r m\u00e5nga f\u00f6rs\u00f6k med en ogiltig token, tillf\u00e4lligt avst\u00e4ngd", "too_many_requests": "F\u00f6r m\u00e5nga f\u00f6rfr\u00e5gningar, f\u00f6rs\u00f6k igen senare", - "unknown": "Ov\u00e4ntat fel" + "unknown": "Ov\u00e4ntat fel", + "unknown_user": "Ok\u00e4nd anv\u00e4ndare. Somfy Protect-konton st\u00f6ds inte av denna integration." }, "flow_title": "Gateway: {gateway_id}", "step": { diff --git a/homeassistant/components/overkiz/translations/tr.json b/homeassistant/components/overkiz/translations/tr.json index 1d04fbbd3cc..3981f7dbc8c 100644 --- a/homeassistant/components/overkiz/translations/tr.json +++ b/homeassistant/components/overkiz/translations/tr.json @@ -11,7 +11,8 @@ "server_in_maintenance": "Sunucu bak\u0131m nedeniyle kapal\u0131", "too_many_attempts": "Ge\u00e7ersiz anahtarla \u00e7ok fazla deneme, ge\u00e7ici olarak yasakland\u0131", "too_many_requests": "\u00c7ok fazla istek var, daha sonra tekrar deneyin", - "unknown": "Beklenmeyen hata" + "unknown": "Beklenmeyen hata", + "unknown_user": "Bilinmeyen kullan\u0131c\u0131. Somfy Protect hesaplar\u0131 bu entegrasyon taraf\u0131ndan desteklenmez." }, "flow_title": "A\u011f ge\u00e7idi: {gateway_id}", "step": { diff --git a/homeassistant/components/overkiz/translations/zh-Hant.json b/homeassistant/components/overkiz/translations/zh-Hant.json index ab265301761..c9e20812ecc 100644 --- a/homeassistant/components/overkiz/translations/zh-Hant.json +++ b/homeassistant/components/overkiz/translations/zh-Hant.json @@ -11,7 +11,8 @@ "server_in_maintenance": "\u4f3a\u670d\u5668\u7dad\u8b77\u4e2d", "too_many_attempts": "\u4f7f\u7528\u7121\u6548\u6b0a\u6756\u5617\u8a66\u6b21\u6578\u904e\u591a\uff0c\u66ab\u6642\u906d\u5230\u5c01\u9396", "too_many_requests": "\u8acb\u6c42\u6b21\u6578\u904e\u591a\uff0c\u8acb\u7a0d\u5f8c\u91cd\u8a66\u3002", - "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + "unknown": "\u672a\u9810\u671f\u932f\u8aa4", + "unknown_user": "\u672a\u77e5\u4f7f\u7528\u8005\u3001\u6b64\u6574\u5408\u4e0d\u652f\u63f4 Somfy Protect \u5e33\u865f\u3002" }, "flow_title": "\u9598\u9053\u5668\uff1a{gateway_id}", "step": { diff --git a/homeassistant/components/ovo_energy/translations/bg.json b/homeassistant/components/ovo_energy/translations/bg.json index b7636becf45..9b0d9f27ccb 100644 --- a/homeassistant/components/ovo_energy/translations/bg.json +++ b/homeassistant/components/ovo_energy/translations/bg.json @@ -1,6 +1,7 @@ { "config": { "error": { + "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\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" }, diff --git a/homeassistant/components/ovo_energy/translations/es.json b/homeassistant/components/ovo_energy/translations/es.json index a28e441d9d8..d3ffba9b2e5 100644 --- a/homeassistant/components/ovo_energy/translations/es.json +++ b/homeassistant/components/ovo_energy/translations/es.json @@ -12,7 +12,7 @@ "password": "Contrase\u00f1a" }, "description": "La autenticaci\u00f3n fall\u00f3 para OVO Energy. Por favor, introduce tus credenciales actuales.", - "title": "Reautenticaci\u00f3n" + "title": "Volver a autenticar" }, "user": { "data": { diff --git a/homeassistant/components/owntracks/device_tracker.py b/homeassistant/components/owntracks/device_tracker.py index 5119168e7ae..79efee6bd2c 100644 --- a/homeassistant/components/owntracks/device_tracker.py +++ b/homeassistant/components/owntracks/device_tracker.py @@ -1,10 +1,6 @@ """Device tracker platform that adds support for OwnTracks over MQTT.""" +from homeassistant.components.device_tracker import ATTR_SOURCE_TYPE, DOMAIN, SourceType from homeassistant.components.device_tracker.config_entry import TrackerEntity -from homeassistant.components.device_tracker.const import ( - ATTR_SOURCE_TYPE, - DOMAIN, - SourceType, -) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_BATTERY_LEVEL, @@ -124,7 +120,7 @@ class OwnTracksEntity(TrackerEntity, RestoreEntity): """Return the device info.""" return DeviceInfo(identifiers={(OT_DOMAIN, self._dev_id)}, name=self.name) - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Call when entity about to be added to Home Assistant.""" await super().async_added_to_hass() diff --git a/homeassistant/components/owntracks/translations/he.json b/homeassistant/components/owntracks/translations/he.json index 82dddbc7034..64466e9bce7 100644 --- a/homeassistant/components/owntracks/translations/he.json +++ b/homeassistant/components/owntracks/translations/he.json @@ -5,7 +5,7 @@ "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." }, "create_entry": { - "default": "\n\u05d1\u05d0\u05e0\u05d3\u05e8\u05d5\u05d0\u05d9\u05d3, \u05d9\u05e9 \u05dc\u05e4\u05ea\u05d5\u05d7 \u05d0\u05ea [\u05d9\u05d9\u05e9\u05d5\u05dd OwnTracks]({android_url}), \u05dc\u05e2\u05d1\u05d5\u05e8 \u05d0\u05dc \u05d4\u05e2\u05d3\u05e4\u05d5\u05ea -> \u05d7\u05d9\u05d1\u05d5\u05e8. \u05d5\u05dc\u05e9\u05e0\u05d5\u05ea \u05d0\u05ea \u05d4\u05d4\u05d2\u05d3\u05e8\u05d5\u05ea \u05d4\u05d1\u05d0\u05d5\u05ea:\n - \u05de\u05e6\u05d1: HTTP \u05e4\u05e8\u05d8\u05d9\n - \u05de\u05d0\u05e8\u05d7: {webhook_url}\n - \u05d6\u05d9\u05d4\u05d5\u05d9:\n - \u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9: `'<\u05d4\u05e9\u05dd \u05e9\u05dc\u05da>'`\n - \u05de\u05d6\u05d4\u05d4 \u05d4\u05ea\u05e7\u05df: `'<\u05e9\u05dd \u05d4\u05d4\u05ea\u05e7\u05df \u05e9\u05dc\u05da>'`\n\n\u05d1\u05de\u05d5\u05e6\u05e8\u05d9 \u05d0\u05e4\u05dc, \u05d9\u05e9 \u05dc\u05e4\u05ea\u05d5\u05d7 \u05d0\u05ea [\u05d9\u05d9\u05e9\u05d5\u05dd OwnTracks]({ios_url}), \u05dc\u05d4\u05e7\u05d9\u05e9 \u05e2\u05dc \u05d4\u05e1\u05de\u05dc (i) \u05d1\u05e4\u05d9\u05e0\u05d4 \u05d4\u05d9\u05de\u05e0\u05d9\u05ea \u05d4\u05e2\u05dc\u05d9\u05d5\u05e0\u05d4 -> \u05d4\u05d2\u05d3\u05e8\u05d5\u05ea. \u05d5\u05dc\u05e9\u05e0\u05d5\u05ea \u05d0\u05ea \u05d4\u05d4\u05d2\u05d3\u05e8\u05d5\u05ea \u05d4\u05d1\u05d0\u05d5\u05ea:\n - \u05de\u05e6\u05d1: HTTP\n - \u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8: {webhook_url}\n - \u05d4\u05e4\u05e2\u05dc\u05ea \u05d0\u05d9\u05de\u05d5\u05ea\n - \u05de\u05d6\u05d4\u05d4 \u05de\u05e9\u05ea\u05de\u05e9: `'<\u05d4\u05e9\u05dd \u05e9\u05dc\u05da>'`\n\n{secret}\n\n\u05e0\u05d9\u05ea\u05df \u05dc\u05e7\u05e8\u05d5\u05d0 \u05d0\u05ea [\u05d4\u05ea\u05d9\u05e2\u05d5\u05d3]({docs_url}) \u05dc\u05e7\u05d1\u05dc\u05ea \u05de\u05d9\u05d3\u05e2 \u05e0\u05d5\u05e1\u05e3." + "default": "\n\u05d1\u05d0\u05e0\u05d3\u05e8\u05d5\u05d0\u05d9\u05d3, \u05d9\u05e9 \u05dc\u05e4\u05ea\u05d5\u05d7 \u05d0\u05ea [\u05d9\u05d9\u05e9\u05d5\u05dd OwnTracks]({android_url}), \u05dc\u05e2\u05d1\u05d5\u05e8 \u05d0\u05dc \u05d4\u05e2\u05d3\u05e4\u05d5\u05ea -> \u05d7\u05d9\u05d1\u05d5\u05e8. \u05d5\u05dc\u05e9\u05e0\u05d5\u05ea \u05d0\u05ea \u05d4\u05d4\u05d2\u05d3\u05e8\u05d5\u05ea \u05d4\u05d1\u05d0\u05d5\u05ea:\n - \u05de\u05e6\u05d1: HTTP \u05e4\u05e8\u05d8\u05d9\n - \u05de\u05d0\u05e8\u05d7: {webhook_url}\n - \u05d6\u05d9\u05d4\u05d5\u05d9:\n - \u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9: `'<\u05d4\u05e9\u05dd \u05e9\u05dc\u05da>'`\n - \u05de\u05d6\u05d4\u05d4 \u05d4\u05ea\u05e7\u05df: `'<\u05e9\u05dd \u05d4\u05d4\u05ea\u05e7\u05df \u05e9\u05dc\u05da>'`\n\n\u05d1\u05de\u05d5\u05e6\u05e8\u05d9 \u05d0\u05e4\u05dc, \u05d9\u05e9 \u05dc\u05e4\u05ea\u05d5\u05d7 \u05d0\u05ea [\u05d9\u05d9\u05e9\u05d5\u05dd OwnTracks]({ios_url}), \u05dc\u05d4\u05e7\u05d9\u05e9 \u05e2\u05dc \u05d4\u05e1\u05de\u05dc\u05d9\u05dc (i) \u05d1\u05e4\u05d9\u05e0\u05d4 \u05d4\u05d9\u05de\u05e0\u05d9\u05ea \u05d4\u05e2\u05dc\u05d9\u05d5\u05e0\u05d4 -> \u05d4\u05d2\u05d3\u05e8\u05d5\u05ea. \u05d5\u05dc\u05e9\u05e0\u05d5\u05ea \u05d0\u05ea \u05d4\u05d4\u05d2\u05d3\u05e8\u05d5\u05ea \u05d4\u05d1\u05d0\u05d5\u05ea:\n - \u05de\u05e6\u05d1: HTTP\n - \u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8: {webhook_url}\n - \u05d4\u05e4\u05e2\u05dc\u05ea \u05d0\u05d9\u05de\u05d5\u05ea\n - \u05de\u05d6\u05d4\u05d4 \u05de\u05e9\u05ea\u05de\u05e9: `'<\u05d4\u05e9\u05dd \u05e9\u05dc\u05da>'`\n\n{secret}\n\n\u05e0\u05d9\u05ea\u05df \u05dc\u05e7\u05e8\u05d5\u05d0 \u05d0\u05ea [\u05d4\u05ea\u05d9\u05e2\u05d5\u05d3]({docs_url}) \u05dc\u05e7\u05d1\u05dc\u05ea \u05de\u05d9\u05d3\u05e2 \u05e0\u05d5\u05e1\u05e3." }, "step": { "user": { diff --git a/homeassistant/components/p1_monitor/sensor.py b/homeassistant/components/p1_monitor/sensor.py index 757bf249ca1..e55c8dacea5 100644 --- a/homeassistant/components/p1_monitor/sensor.py +++ b/homeassistant/components/p1_monitor/sensor.py @@ -222,6 +222,7 @@ SENSORS_WATERMETER: tuple[SensorEntityDescription, ...] = ( name="Consumption Day", state_class=SensorStateClass.TOTAL_INCREASING, native_unit_of_measurement=VOLUME_LITERS, + device_class=SensorDeviceClass.VOLUME, ), SensorEntityDescription( key="consumption_total", diff --git a/homeassistant/components/p1_monitor/translations/pl.json b/homeassistant/components/p1_monitor/translations/pl.json index 9ec0f1bcef0..93e2b7a83bd 100644 --- a/homeassistant/components/p1_monitor/translations/pl.json +++ b/homeassistant/components/p1_monitor/translations/pl.json @@ -9,6 +9,9 @@ "host": "Nazwa hosta lub adres IP", "name": "Nazwa" }, + "data_description": { + "host": "Adres IP lub nazwa hosta instalacji monitora P1." + }, "description": "Skonfiguruj P1 Monitor, aby zintegrowa\u0107 go z Home Assistantem." } } diff --git a/homeassistant/components/p1_monitor/translations/pt.json b/homeassistant/components/p1_monitor/translations/pt.json index 38336a1d5de..ab627843537 100644 --- a/homeassistant/components/p1_monitor/translations/pt.json +++ b/homeassistant/components/p1_monitor/translations/pt.json @@ -8,6 +8,9 @@ "data": { "host": "Servidor", "name": "Nome" + }, + "data_description": { + "host": "O endere\u00e7o IP ou nome de host da instala\u00e7\u00e3o do Monitor P1." } } } diff --git a/homeassistant/components/p1_monitor/translations/sv.json b/homeassistant/components/p1_monitor/translations/sv.json index 2dcba36c76a..2a4c3c62277 100644 --- a/homeassistant/components/p1_monitor/translations/sv.json +++ b/homeassistant/components/p1_monitor/translations/sv.json @@ -9,6 +9,9 @@ "host": "V\u00e4rd", "name": "Namn" }, + "data_description": { + "host": "IP-adressen eller v\u00e4rdnamnet f\u00f6r din P1 Monitor-installation." + }, "description": "Konfigurera P1 Monitor f\u00f6r att integrera med Home Assistant." } } diff --git a/homeassistant/components/p1_monitor/translations/tr.json b/homeassistant/components/p1_monitor/translations/tr.json index f00060462fd..1ee8c351c8d 100644 --- a/homeassistant/components/p1_monitor/translations/tr.json +++ b/homeassistant/components/p1_monitor/translations/tr.json @@ -9,6 +9,9 @@ "host": "Sunucu", "name": "Ad" }, + "data_description": { + "host": "P1 Monitor kurulumunuzun IP adresi veya ana bilgisayar ad\u0131." + }, "description": "Home Assistant ile entegre etmek i\u00e7in P1 Monitor'\u00fc kurun." } } diff --git a/homeassistant/components/panasonic_bluray/media_player.py b/homeassistant/components/panasonic_bluray/media_player.py index d87d2a8efc5..54062b36b1b 100644 --- a/homeassistant/components/panasonic_bluray/media_player.py +++ b/homeassistant/components/panasonic_bluray/media_player.py @@ -10,14 +10,9 @@ from homeassistant.components.media_player import ( PLATFORM_SCHEMA, MediaPlayerEntity, MediaPlayerEntityFeature, + MediaPlayerState, ) -from homeassistant.const import ( - CONF_HOST, - CONF_NAME, - STATE_IDLE, - STATE_OFF, - STATE_PLAYING, -) +from homeassistant.const import CONF_HOST, CONF_NAME from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -65,7 +60,7 @@ class PanasonicBluRay(MediaPlayerEntity): """Initialize the Panasonic Blue-ray device.""" self._device = PanasonicBD(ip) self._name = name - self._state = STATE_OFF + self._state = MediaPlayerState.OFF self._position = 0 self._duration = 0 self._position_valid = 0 @@ -100,7 +95,7 @@ class PanasonicBluRay(MediaPlayerEntity): """When was the position of the current playing media valid.""" return self._position_valid - def update(self): + def update(self) -> None: """Update the internal state by querying the device.""" # This can take 5+ seconds to complete state = self._device.get_play_status() @@ -111,11 +106,11 @@ class PanasonicBluRay(MediaPlayerEntity): # We map both of these to off. If it's really off we can't # turn it on, but from standby we can go to idle by pressing # POWER. - self._state = STATE_OFF + self._state = MediaPlayerState.OFF elif state[0] in ["paused", "stopped"]: - self._state = STATE_IDLE + self._state = MediaPlayerState.IDLE elif state[0] == "playing": - self._state = STATE_PLAYING + self._state = MediaPlayerState.PLAYING # Update our current media position + length if state[1] >= 0: @@ -125,7 +120,7 @@ class PanasonicBluRay(MediaPlayerEntity): self._position_valid = utcnow() self._duration = state[2] - def turn_off(self): + def turn_off(self) -> None: """ Instruct the device to turn standby. @@ -134,26 +129,26 @@ class PanasonicBluRay(MediaPlayerEntity): our favour as it means the device is still accepting commands and we can thus turn it back on when desired. """ - if self._state != STATE_OFF: + if self._state != MediaPlayerState.OFF: self._device.send_key("POWER") - self._state = STATE_OFF + self._state = MediaPlayerState.OFF - def turn_on(self): + def turn_on(self) -> None: """Wake the device back up from standby.""" - if self._state == STATE_OFF: + if self._state == MediaPlayerState.OFF: self._device.send_key("POWER") - self._state = STATE_IDLE + self._state = MediaPlayerState.IDLE - def media_play(self): + def media_play(self) -> None: """Send play command.""" self._device.send_key("PLAYBACK") - def media_pause(self): + def media_pause(self) -> None: """Send pause command.""" self._device.send_key("PAUSE") - def media_stop(self): + def media_stop(self) -> None: """Send stop command.""" self._device.send_key("STOP") diff --git a/homeassistant/components/panasonic_viera/media_player.py b/homeassistant/components/panasonic_viera/media_player.py index 7b75809f827..14c440f0ec1 100644 --- a/homeassistant/components/panasonic_viera/media_player.py +++ b/homeassistant/components/panasonic_viera/media_player.py @@ -2,19 +2,19 @@ from __future__ import annotations import logging +from typing import Any from panasonic_viera import Keys from homeassistant.components import media_source from homeassistant.components.media_player import ( + BrowseMedia, MediaPlayerDeviceClass, MediaPlayerEntity, MediaPlayerEntityFeature, -) -from homeassistant.components.media_player.browse_media import ( + MediaType, async_process_play_media_url, ) -from homeassistant.components.media_player.const import MEDIA_TYPE_URL from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant @@ -111,7 +111,7 @@ class PanasonicVieraTVEntity(MediaPlayerEntity): return self._remote.state @property - def available(self): + def available(self) -> bool: """Return True if the device is available.""" return self._remote.available @@ -125,35 +125,35 @@ class PanasonicVieraTVEntity(MediaPlayerEntity): """Boolean if volume is currently muted.""" return self._remote.muted - async def async_update(self): + async def async_update(self) -> None: """Retrieve the latest data.""" await self._remote.async_update() - async def async_turn_on(self): + async def async_turn_on(self) -> None: """Turn on the media player.""" await self._remote.async_turn_on(context=self._context) - async def async_turn_off(self): + async def async_turn_off(self) -> None: """Turn off media player.""" await self._remote.async_turn_off() - async def async_volume_up(self): + async def async_volume_up(self) -> None: """Volume up the media player.""" await self._remote.async_send_key(Keys.volume_up) - async def async_volume_down(self): + async def async_volume_down(self) -> None: """Volume down media player.""" await self._remote.async_send_key(Keys.volume_down) - async def async_mute_volume(self, mute): + async def async_mute_volume(self, mute: bool) -> None: """Send mute command.""" await self._remote.async_set_mute(mute) - async def async_set_volume_level(self, volume): + async def async_set_volume_level(self, volume: float) -> None: """Set volume level, range 0..1.""" await self._remote.async_set_volume(volume) - async def async_media_play_pause(self): + async def async_media_play_pause(self) -> None: """Simulate play pause media player.""" if self._remote.playing: await self._remote.async_send_key(Keys.pause) @@ -162,44 +162,48 @@ class PanasonicVieraTVEntity(MediaPlayerEntity): await self._remote.async_send_key(Keys.play) self._remote.playing = True - async def async_media_play(self): + async def async_media_play(self) -> None: """Send play command.""" await self._remote.async_send_key(Keys.play) self._remote.playing = True - async def async_media_pause(self): + async def async_media_pause(self) -> None: """Send pause command.""" await self._remote.async_send_key(Keys.pause) self._remote.playing = False - async def async_media_stop(self): + async def async_media_stop(self) -> None: """Stop playback.""" await self._remote.async_send_key(Keys.stop) - async def async_media_next_track(self): + async def async_media_next_track(self) -> None: """Send the fast forward command.""" await self._remote.async_send_key(Keys.fast_forward) - async def async_media_previous_track(self): + async def async_media_previous_track(self) -> None: """Send the rewind command.""" await self._remote.async_send_key(Keys.rewind) - async def async_play_media(self, media_type, media_id, **kwargs): + async def async_play_media( + self, media_type: str, media_id: str, **kwargs: Any + ) -> None: """Play media.""" if media_source.is_media_source_id(media_id): - media_type = MEDIA_TYPE_URL + media_type = MediaType.URL play_item = await media_source.async_resolve_media( self.hass, media_id, self.entity_id ) media_id = play_item.url - if media_type != MEDIA_TYPE_URL: + if media_type != MediaType.URL: _LOGGER.warning("Unsupported media_type: %s", media_type) return media_id = async_process_play_media_url(self.hass, media_id) await self._remote.async_play_media(media_type, media_id) - 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.""" return await media_source.async_browse_media(self.hass, media_content_id) diff --git a/homeassistant/components/panasonic_viera/remote.py b/homeassistant/components/panasonic_viera/remote.py index bdb44a72a74..717ee090612 100644 --- a/homeassistant/components/panasonic_viera/remote.py +++ b/homeassistant/components/panasonic_viera/remote.py @@ -1,6 +1,9 @@ """Remote control support for Panasonic Viera TV.""" from __future__ import annotations +from collections.abc import Iterable +from typing import Any + from homeassistant.components.remote import RemoteEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME, STATE_ON @@ -71,7 +74,7 @@ class PanasonicVieraRemoteEntity(RemoteEntity): return self._name @property - def available(self): + def available(self) -> bool: """Return True if the device is available.""" return self._remote.available @@ -80,15 +83,15 @@ class PanasonicVieraRemoteEntity(RemoteEntity): """Return true if device is on.""" return self._remote.state == STATE_ON - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" await self._remote.async_turn_on(context=self._context) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" await self._remote.async_turn_off() - async def async_send_command(self, command, **kwargs): + async def async_send_command(self, command: Iterable[str], **kwargs: Any) -> None: """Send a command to one device.""" for cmd in command: await self._remote.async_send_key(cmd) diff --git a/homeassistant/components/pandora/media_player.py b/homeassistant/components/pandora/media_player.py index 542d749c573..c45c04a330d 100644 --- a/homeassistant/components/pandora/media_player.py +++ b/homeassistant/components/pandora/media_player.py @@ -14,8 +14,9 @@ from homeassistant import util from homeassistant.components.media_player import ( MediaPlayerEntity, MediaPlayerEntityFeature, + MediaPlayerState, + MediaType, ) -from homeassistant.components.media_player.const import MEDIA_TYPE_MUSIC from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, SERVICE_MEDIA_NEXT_TRACK, @@ -23,10 +24,6 @@ from homeassistant.const import ( SERVICE_MEDIA_PLAY_PAUSE, SERVICE_VOLUME_DOWN, SERVICE_VOLUME_UP, - STATE_IDLE, - STATE_OFF, - STATE_PAUSED, - STATE_PLAYING, ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -70,6 +67,7 @@ def setup_platform( class PandoraMediaPlayer(MediaPlayerEntity): """A media player that uses the Pianobar interface to Pandora.""" + _attr_media_content_type = MediaType.MUSIC # MediaPlayerEntityFeature.VOLUME_SET is close to available but we need volume up/down # controls in the GUI. _attr_supported_features = ( @@ -84,7 +82,7 @@ class PandoraMediaPlayer(MediaPlayerEntity): def __init__(self, name): """Initialize the Pandora device.""" self._name = name - self._player_state = STATE_OFF + self._player_state = MediaPlayerState.OFF self._station = "" self._media_title = "" self._media_artist = "" @@ -104,9 +102,9 @@ class PandoraMediaPlayer(MediaPlayerEntity): """Return the state of the player.""" return self._player_state - def turn_on(self): + def turn_on(self) -> None: """Turn the media player on.""" - if self._player_state != STATE_OFF: + if self._player_state != MediaPlayerState.OFF: return self._pianobar = pexpect.spawn("pianobar") _LOGGER.info("Started pianobar subprocess") @@ -131,10 +129,10 @@ class PandoraMediaPlayer(MediaPlayerEntity): self._update_stations() self.update_playing_status() - self._player_state = STATE_IDLE + self._player_state = MediaPlayerState.IDLE self.schedule_update_ha_state() - def turn_off(self): + def turn_off(self) -> None: """Turn the media player off.""" if self._pianobar is None: _LOGGER.info("Pianobar subprocess already stopped") @@ -148,22 +146,22 @@ class PandoraMediaPlayer(MediaPlayerEntity): os.killpg(os.getpgid(self._pianobar.pid), signal.SIGTERM) _LOGGER.debug("Killed Pianobar subprocess") self._pianobar = None - self._player_state = STATE_OFF + self._player_state = MediaPlayerState.OFF self.schedule_update_ha_state() - def media_play(self): + def media_play(self) -> None: """Send play command.""" self._send_pianobar_command(SERVICE_MEDIA_PLAY_PAUSE) - self._player_state = STATE_PLAYING + self._player_state = MediaPlayerState.PLAYING self.schedule_update_ha_state() - def media_pause(self): + def media_pause(self) -> None: """Send pause command.""" self._send_pianobar_command(SERVICE_MEDIA_PLAY_PAUSE) - self._player_state = STATE_PAUSED + self._player_state = MediaPlayerState.PAUSED self.schedule_update_ha_state() - def media_next_track(self): + def media_next_track(self) -> None: """Go to next track.""" self._send_pianobar_command(SERVICE_MEDIA_NEXT_TRACK) self.schedule_update_ha_state() @@ -184,11 +182,6 @@ class PandoraMediaPlayer(MediaPlayerEntity): self.update_playing_status() return self._media_title - @property - def media_content_type(self): - """Content type of current playing media.""" - return MEDIA_TYPE_MUSIC - @property def media_artist(self): """Artist of current playing media, music track only.""" @@ -204,7 +197,7 @@ class PandoraMediaPlayer(MediaPlayerEntity): """Duration of current playing media in seconds.""" return self._media_duration - def select_source(self, source): + def select_source(self, source: str) -> None: """Choose a different Pandora station and play it.""" try: station_index = self._stations.index(source) @@ -215,7 +208,7 @@ class PandoraMediaPlayer(MediaPlayerEntity): self._send_station_list_command() self._pianobar.sendline(f"{station_index}") self._pianobar.expect("\r\n") - self._player_state = STATE_PLAYING + self._player_state = MediaPlayerState.PLAYING def _send_station_list_command(self): """Send a station list command.""" @@ -312,9 +305,9 @@ class PandoraMediaPlayer(MediaPlayerEntity): self._media_duration = int(total_minutes) * 60 + int(total_seconds) if time_remaining not in (self._time_remaining, self._media_duration): - self._player_state = STATE_PLAYING - elif self._player_state == STATE_PLAYING: - self._player_state = STATE_PAUSED + self._player_state = MediaPlayerState.PLAYING + elif self._player_state == MediaPlayerState.PLAYING: + self._player_state = MediaPlayerState.PAUSED self._time_remaining = time_remaining def _log_match(self): diff --git a/homeassistant/components/pencom/switch.py b/homeassistant/components/pencom/switch.py index 8f83259ef30..064ac43e6b8 100644 --- a/homeassistant/components/pencom/switch.py +++ b/homeassistant/components/pencom/switch.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any from pencompy.pencompy import Pencompy import voluptuous as vol @@ -90,15 +91,15 @@ class PencomRelay(SwitchEntity): """Return a relay's state.""" return self._state - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn a relay on.""" self._hub.set(self._board, self._addr, True) - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn a relay off.""" self._hub.set(self._board, self._addr, False) - def update(self): + def update(self) -> None: """Refresh a relay's state.""" self._state = self._hub.get(self._board, self._addr) diff --git a/homeassistant/components/person/__init__.py b/homeassistant/components/person/__init__.py index 09851d70384..86a132027d8 100644 --- a/homeassistant/components/person/__init__.py +++ b/homeassistant/components/person/__init__.py @@ -2,7 +2,6 @@ from __future__ import annotations import logging -from typing import cast import voluptuous as vol @@ -107,7 +106,7 @@ async def async_add_user_device_tracker( hass: HomeAssistant, user_id: str, device_tracker_entity_id: str ): """Add a device tracker to a person linked to a user.""" - coll = cast(PersonStorageCollection, hass.data[DOMAIN][1]) + coll: PersonStorageCollection = hass.data[DOMAIN][1] for person in coll.async_items(): if person.get(ATTR_USER_ID) != user_id: @@ -134,12 +133,12 @@ def persons_with_entity(hass: HomeAssistant, entity_id: str) -> list[str]: ): return [] - component: EntityComponent = hass.data[DOMAIN][2] + component: EntityComponent[Person] = hass.data[DOMAIN][2] return [ person_entity.entity_id for person_entity in component.entities - if entity_id in cast(Person, person_entity).device_trackers + if entity_id in person_entity.device_trackers ] @@ -149,12 +148,12 @@ def entities_in_person(hass: HomeAssistant, entity_id: str) -> list[str]: if DOMAIN not in hass.data: return [] - component: EntityComponent = hass.data[DOMAIN][2] + component: EntityComponent[Person] = hass.data[DOMAIN][2] if (person_entity := component.get_entity(entity_id)) is None: return [] - return cast(Person, person_entity).device_trackers + return person_entity.device_trackers CREATE_FIELDS = { @@ -326,7 +325,7 @@ The following persons point at invalid users: async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the person component.""" - entity_component = EntityComponent(_LOGGER, DOMAIN, hass) + entity_component = EntityComponent[Person](_LOGGER, DOMAIN, hass) id_manager = collection.IDManager() yaml_collection = collection.YamlCollection( logging.getLogger(f"{__name__}.yaml_collection"), id_manager @@ -342,7 +341,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: hass, DOMAIN, DOMAIN, entity_component, yaml_collection, Person ) collection.sync_entity_lifecycle( - hass, DOMAIN, DOMAIN, entity_component, storage_collection, Person.from_yaml + hass, DOMAIN, DOMAIN, entity_component, storage_collection, Person ) await yaml_collection.async_load( @@ -385,15 +384,15 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return True -class Person(RestoreEntity): +class Person(collection.CollectionEntity, RestoreEntity): """Represent a tracked person.""" _attr_should_poll = False + editable: bool def __init__(self, config): """Set up person.""" self._config = config - self.editable = True self._latitude = None self._longitude = None self._gps_accuracy = None @@ -402,8 +401,15 @@ class Person(RestoreEntity): self._unsub_track_device = None @classmethod - def from_yaml(cls, config): - """Return entity instance initialized from yaml storage.""" + def from_storage(cls, config: ConfigType): + """Return entity instance initialized from storage.""" + person = cls(config) + person.editable = True + return person + + @classmethod + def from_yaml(cls, config: ConfigType): + """Return entity instance initialized from yaml.""" person = cls(config) person.editable = False return person @@ -468,7 +474,7 @@ class Person(RestoreEntity): EVENT_HOMEASSISTANT_START, person_start_hass ) - async def async_update_config(self, config): + async def async_update_config(self, config: ConfigType): """Handle when the config is updated.""" self._config = config diff --git a/homeassistant/components/person/manifest.json b/homeassistant/components/person/manifest.json index 09b74bf34eb..dc9c76ca103 100644 --- a/homeassistant/components/person/manifest.json +++ b/homeassistant/components/person/manifest.json @@ -6,5 +6,6 @@ "after_dependencies": ["device_tracker"], "codeowners": [], "quality_scale": "internal", - "iot_class": "calculated" + "iot_class": "calculated", + "integration_type": "system" } diff --git a/homeassistant/components/philips_js/media_player.py b/homeassistant/components/philips_js/media_player.py index 8d864436ac5..116833d8a97 100644 --- a/homeassistant/components/philips_js/media_player.py +++ b/homeassistant/components/philips_js/media_player.py @@ -1,26 +1,21 @@ """Media Player component to integrate TVs exposing the Joint Space API.""" from __future__ import annotations +from typing import Any + from haphilipsjs import ConnectionFailure from homeassistant.components.media_player import ( + BrowseError, BrowseMedia, + MediaClass, MediaPlayerDeviceClass, MediaPlayerEntity, MediaPlayerEntityFeature, + MediaPlayerState, + MediaType, ) -from homeassistant.components.media_player.const import ( - MEDIA_CLASS_APP, - MEDIA_CLASS_CHANNEL, - MEDIA_CLASS_DIRECTORY, - MEDIA_TYPE_APP, - MEDIA_TYPE_APPS, - MEDIA_TYPE_CHANNEL, - MEDIA_TYPE_CHANNELS, -) -from homeassistant.components.media_player.errors import BrowseError from homeassistant.config_entries import ConfigEntry -from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -94,7 +89,7 @@ class PhilipsTVMediaPlayer( sw_version=coordinator.system.get("softwareversion"), name=coordinator.system["name"], ) - self._state = STATE_OFF + self._state = MediaPlayerState.OFF self._media_content_type: str | None = None self._media_content_id: str | None = None self._media_title: str | None = None @@ -119,11 +114,11 @@ class PhilipsTVMediaPlayer( return supports @property - def state(self): + 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 STATE_ON - return STATE_OFF + return MediaPlayerState.ON + return MediaPlayerState.OFF @property def source(self): @@ -135,7 +130,7 @@ class PhilipsTVMediaPlayer( """List of available input sources.""" return list(self._sources.values()) - async def async_select_source(self, source): + 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) @@ -151,35 +146,35 @@ class PhilipsTVMediaPlayer( """Boolean if volume is currently muted.""" return self._tv.muted - async def async_turn_on(self): + 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 = STATE_ON + self._state = MediaPlayerState.ON else: await self.coordinator.turn_on.async_run(self.hass, self._context) await self._async_update_soon() - async def async_turn_off(self): + async def async_turn_off(self) -> None: """Turn off the device.""" - if self._state == STATE_ON: + if self._state == MediaPlayerState.ON: await self._tv.sendKey("Standby") - self._state = STATE_OFF + self._state = MediaPlayerState.OFF await self._async_update_soon() else: _LOGGER.debug("Ignoring turn off when already in expected state") - async def async_volume_up(self): + async def async_volume_up(self) -> None: """Send volume up command.""" await self._tv.sendKey("VolumeUp") await self._async_update_soon() - async def async_volume_down(self): + async def async_volume_down(self) -> None: """Send volume down command.""" await self._tv.sendKey("VolumeDown") await self._async_update_soon() - async def async_mute_volume(self, mute): + async def async_mute_volume(self, mute: bool) -> None: """Send mute command.""" if self._tv.muted != mute: await self._tv.sendKey("Mute") @@ -187,22 +182,22 @@ class PhilipsTVMediaPlayer( else: _LOGGER.debug("Ignoring request when already in expected state") - async def async_set_volume_level(self, volume): + async def async_set_volume_level(self, volume: float) -> None: """Set volume level, range 0..1.""" await self._tv.setVolume(volume, self._tv.muted) await self._async_update_soon() - async def async_media_previous_track(self): + async def async_media_previous_track(self) -> None: """Send rewind command.""" await self._tv.sendKey("Previous") await self._async_update_soon() - async def async_media_next_track(self): + async def async_media_next_track(self) -> None: """Send fast forward command.""" await self._tv.sendKey("Next") await self._async_update_soon() - async def async_media_play_pause(self): + async def async_media_play_pause(self) -> None: """Send pause command to media player.""" if self._tv.quirk_playpause_spacebar: await self._tv.sendUnicode(" ") @@ -210,17 +205,17 @@ class PhilipsTVMediaPlayer( await self._tv.sendKey("PlayPause") await self._async_update_soon() - async def async_media_play(self): + async def async_media_play(self) -> None: """Send pause command to media player.""" await self._tv.sendKey("Play") await self._async_update_soon() - async def async_media_pause(self): + async def async_media_pause(self) -> None: """Send play command to media player.""" await self._tv.sendKey("Pause") await self._async_update_soon() - async def async_media_stop(self): + async def async_media_stop(self) -> None: """Send play command to media player.""" await self._tv.sendKey("Stop") await self._async_update_soon() @@ -249,8 +244,8 @@ class PhilipsTVMediaPlayer( def media_image_url(self): """Image url of current playing media.""" if self._media_content_id and self._media_content_type in ( - MEDIA_TYPE_APP, - MEDIA_TYPE_CHANNEL, + MediaType.APP, + MediaType.CHANNEL, ): return self.get_browse_image_url( self._media_content_type, self._media_content_id, media_image_id=None @@ -268,18 +263,20 @@ class PhilipsTVMediaPlayer( if app := self._tv.applications.get(self._tv.application_id): return app.get("label") - async def async_play_media(self, media_type, media_id, **kwargs): + async def async_play_media( + self, media_type: str, media_id: str, **kwargs: Any + ) -> None: """Play a piece of media.""" _LOGGER.debug("Call play media type <%s>, Id <%s>", media_type, media_id) - if media_type == MEDIA_TYPE_CHANNEL: + 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) - elif media_type == MEDIA_TYPE_APP: + elif media_type == MediaType.APP: if app := self._tv.applications.get(media_id): await self._tv.setApplication(app["intent"]) await self._async_update_soon() @@ -294,9 +291,9 @@ class PhilipsTVMediaPlayer( children = [ BrowseMedia( title=channel.get("name", f"Channel: {channel_id}"), - media_class=MEDIA_CLASS_CHANNEL, + media_class=MediaClass.CHANNEL, media_content_id=f"alltv/{channel_id}", - media_content_type=MEDIA_TYPE_CHANNEL, + media_content_type=MediaType.CHANNEL, can_play=True, can_expand=False, ) @@ -307,10 +304,10 @@ class PhilipsTVMediaPlayer( return BrowseMedia( title="Channels", - media_class=MEDIA_CLASS_DIRECTORY, + media_class=MediaClass.DIRECTORY, media_content_id="channels", - media_content_type=MEDIA_TYPE_CHANNELS, - children_media_class=MEDIA_CLASS_CHANNEL, + media_content_type=MediaType.CHANNELS, + children_media_class=MediaClass.CHANNEL, can_play=False, can_expand=True, children=children, @@ -331,9 +328,9 @@ class PhilipsTVMediaPlayer( children = [ BrowseMedia( title=get_name(channel), - media_class=MEDIA_CLASS_CHANNEL, + media_class=MediaClass.CHANNEL, media_content_id=f"{list_id}/{channel['ccid']}", - media_content_type=MEDIA_TYPE_CHANNEL, + media_content_type=MediaType.CHANNEL, can_play=True, can_expand=False, ) @@ -347,10 +344,10 @@ class PhilipsTVMediaPlayer( favorite = self._tv.favorite_lists[list_id] return BrowseMedia( title=favorite.get("name", f"Favorites {list_id}"), - media_class=MEDIA_CLASS_DIRECTORY, + media_class=MediaClass.DIRECTORY, media_content_id=f"favorites/{list_id}", - media_content_type=MEDIA_TYPE_CHANNELS, - children_media_class=MEDIA_CLASS_CHANNEL, + media_content_type=MediaType.CHANNELS, + children_media_class=MediaClass.CHANNEL, can_play=False, can_expand=True, children=children, @@ -362,13 +359,13 @@ class PhilipsTVMediaPlayer( children = [ BrowseMedia( title=application["label"], - media_class=MEDIA_CLASS_APP, + media_class=MediaClass.APP, media_content_id=application_id, - media_content_type=MEDIA_TYPE_APP, + media_content_type=MediaType.APP, can_play=True, can_expand=False, thumbnail=self.get_browse_image_url( - MEDIA_TYPE_APP, application_id, media_image_id=None + MediaType.APP, application_id, media_image_id=None ), ) for application_id, application in self._tv.applications.items() @@ -378,10 +375,10 @@ class PhilipsTVMediaPlayer( return BrowseMedia( title="Applications", - media_class=MEDIA_CLASS_DIRECTORY, + media_class=MediaClass.DIRECTORY, media_content_id="applications", - media_content_type=MEDIA_TYPE_APPS, - children_media_class=MEDIA_CLASS_APP, + media_content_type=MediaType.APPS, + children_media_class=MediaClass.APP, can_play=False, can_expand=True, children=children, @@ -399,10 +396,10 @@ class PhilipsTVMediaPlayer( return BrowseMedia( title="Favorites", - media_class=MEDIA_CLASS_DIRECTORY, + media_class=MediaClass.DIRECTORY, media_content_id="favorite_lists", - media_content_type=MEDIA_TYPE_CHANNELS, - children_media_class=MEDIA_CLASS_CHANNEL, + media_content_type=MediaType.CHANNELS, + children_media_class=MediaClass.CHANNEL, can_play=False, can_expand=True, children=children, @@ -413,7 +410,7 @@ class PhilipsTVMediaPlayer( return BrowseMedia( title="Philips TV", - media_class=MEDIA_CLASS_DIRECTORY, + media_class=MediaClass.DIRECTORY, media_content_id="", media_content_type="", can_play=False, @@ -452,15 +449,15 @@ class PhilipsTVMediaPlayer( ) -> tuple[bytes | None, str | None]: """Serve album art. Returns (content, content_type).""" try: - if media_content_type == MEDIA_TYPE_APP and media_content_id: + if media_content_type == MediaType.APP and media_content_id: return await self._tv.getApplicationIcon(media_content_id) - if media_content_type == MEDIA_TYPE_CHANNEL and media_content_id: + if media_content_type == MediaType.CHANNEL and media_content_id: return await self._tv.getChannelLogo(media_content_id) except ConnectionFailure: _LOGGER.warning("Failed to fetch image") return None, None - async def async_get_media_image(self): + async def async_get_media_image(self) -> tuple[bytes | None, str | None]: """Serve album art. Returns (content, content_type).""" return await self.async_get_browse_image( self.media_content_type, self.media_content_id, None @@ -471,11 +468,11 @@ class PhilipsTVMediaPlayer( if self._tv.on: if self._tv.powerstate in ("Standby", "StandbyKeep"): - self._state = STATE_OFF + self._state = MediaPlayerState.OFF else: - self._state = STATE_ON + self._state = MediaPlayerState.ON else: - self._state = STATE_OFF + self._state = MediaPlayerState.OFF self._sources = { srcid: source.get("name") or f"Source {srcid}" @@ -483,14 +480,14 @@ class PhilipsTVMediaPlayer( } if self._tv.channel_active: - self._media_content_type = MEDIA_TYPE_CHANNEL + 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( "name" ) self._media_channel = self._media_title elif self._tv.application_id: - self._media_content_type = MEDIA_TYPE_APP + self._media_content_type = MediaType.APP self._media_content_id = self._tv.application_id self._media_title = self._tv.applications.get( self._tv.application_id, {} diff --git a/homeassistant/components/philips_js/remote.py b/homeassistant/components/philips_js/remote.py index 7e8c5448cca..02d5e512a33 100644 --- a/homeassistant/components/philips_js/remote.py +++ b/homeassistant/components/philips_js/remote.py @@ -1,5 +1,7 @@ """Remote control support for Apple TV.""" import asyncio +from collections.abc import Iterable +from typing import Any from homeassistant.components.remote import ( ATTR_DELAY_SECS, @@ -58,7 +60,7 @@ class PhilipsTVRemote(CoordinatorEntity[PhilipsTVDataUpdateCoordinator], RemoteE self._tv.on and (self._tv.powerstate == "On" or self._tv.powerstate is None) ) - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" if self._tv.on and self._tv.powerstate: await self._tv.setPowerState("On") @@ -66,7 +68,7 @@ class PhilipsTVRemote(CoordinatorEntity[PhilipsTVDataUpdateCoordinator], RemoteE await self.coordinator.turn_on.async_run(self.hass, self._context) self.async_write_ha_state() - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" if self._tv.on: await self._tv.sendKey("Standby") @@ -74,7 +76,7 @@ class PhilipsTVRemote(CoordinatorEntity[PhilipsTVDataUpdateCoordinator], RemoteE else: LOGGER.debug("Tv was already turned off") - async def async_send_command(self, command, **kwargs): + async def async_send_command(self, command: Iterable[str], **kwargs: Any) -> None: """Send a command to one device.""" num_repeats = kwargs[ATTR_NUM_REPEATS] delay = kwargs.get(ATTR_DELAY_SECS, DEFAULT_DELAY_SECS) diff --git a/homeassistant/components/picnic/translations/es.json b/homeassistant/components/picnic/translations/es.json index 93024c5611c..54899c8e8fd 100644 --- a/homeassistant/components/picnic/translations/es.json +++ b/homeassistant/components/picnic/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "error": { "cannot_connect": "No se pudo conectar", diff --git a/homeassistant/components/ping/binary_sensor.py b/homeassistant/components/ping/binary_sensor.py index ff88412a6ef..cbb511a4ae3 100644 --- a/homeassistant/components/ping/binary_sensor.py +++ b/homeassistant/components/ping/binary_sensor.py @@ -131,7 +131,7 @@ class PingBinarySensor(RestoreEntity, BinarySensorEntity): await self._ping.async_update() self._available = True - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Restore previous state on restart to avoid blocking startup.""" await super().async_added_to_hass() diff --git a/homeassistant/components/ping/device_tracker.py b/homeassistant/components/ping/device_tracker.py index 52285350cd4..b4266c8e9a7 100644 --- a/homeassistant/components/ping/device_tracker.py +++ b/homeassistant/components/ping/device_tracker.py @@ -11,12 +11,10 @@ import voluptuous as vol from homeassistant import const, util from homeassistant.components.device_tracker import ( - PLATFORM_SCHEMA as BASE_PLATFORM_SCHEMA, - AsyncSeeCallback, -) -from homeassistant.components.device_tracker.const import ( CONF_SCAN_INTERVAL, + PLATFORM_SCHEMA as BASE_PLATFORM_SCHEMA, SCAN_INTERVAL, + AsyncSeeCallback, SourceType, ) from homeassistant.core import HomeAssistant diff --git a/homeassistant/components/pioneer/media_player.py b/homeassistant/components/pioneer/media_player.py index 7e3931b1ae3..620e314fdd8 100644 --- a/homeassistant/components/pioneer/media_player.py +++ b/homeassistant/components/pioneer/media_player.py @@ -10,15 +10,9 @@ from homeassistant.components.media_player import ( PLATFORM_SCHEMA, MediaPlayerEntity, MediaPlayerEntityFeature, + MediaPlayerState, ) -from homeassistant.const import ( - CONF_HOST, - CONF_NAME, - CONF_PORT, - CONF_TIMEOUT, - STATE_OFF, - STATE_ON, -) +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, CONF_TIMEOUT from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -174,14 +168,14 @@ class PioneerDevice(MediaPlayerEntity): return self._name @property - def state(self): + def state(self) -> MediaPlayerState | None: """Return the state of the device.""" if self._pwstate == "PWR2": - return STATE_OFF + return MediaPlayerState.OFF if self._pwstate == "PWR1": - return STATE_OFF + return MediaPlayerState.OFF if self._pwstate == "PWR0": - return STATE_ON + return MediaPlayerState.ON return None @@ -210,31 +204,31 @@ class PioneerDevice(MediaPlayerEntity): """Title of current playing media.""" return self._selected_source - def turn_off(self): + def turn_off(self) -> None: """Turn off media player.""" self.telnet_command("PF") - def volume_up(self): + def volume_up(self) -> None: """Volume up media player.""" self.telnet_command("VU") - def volume_down(self): + def volume_down(self) -> None: """Volume down media player.""" self.telnet_command("VD") - def set_volume_level(self, volume): + def set_volume_level(self, volume: float) -> None: """Set volume level, range 0..1.""" # 60dB max self.telnet_command(f"{round(volume * MAX_VOLUME):03}VL") - def mute_volume(self, mute): + def mute_volume(self, mute: bool) -> None: """Mute (true) or unmute (false) media player.""" self.telnet_command("MO" if mute else "MF") - def turn_on(self): + def turn_on(self) -> None: """Turn the media player on.""" self.telnet_command("PO") - def select_source(self, source): + def select_source(self, source: str) -> None: """Select input source.""" self.telnet_command(f"{self._source_name_to_number.get(source)}FN") diff --git a/homeassistant/components/pjlink/media_player.py b/homeassistant/components/pjlink/media_player.py index 73fb1888341..0e1151e3dc4 100644 --- a/homeassistant/components/pjlink/media_player.py +++ b/homeassistant/components/pjlink/media_player.py @@ -9,15 +9,9 @@ from homeassistant.components.media_player import ( PLATFORM_SCHEMA, MediaPlayerEntity, MediaPlayerEntityFeature, + MediaPlayerState, ) -from homeassistant.const import ( - CONF_HOST, - CONF_NAME, - CONF_PASSWORD, - CONF_PORT, - STATE_OFF, - STATE_ON, -) +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_PORT from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -89,7 +83,7 @@ class PjLinkDevice(MediaPlayerEntity): self._password = password self._encoding = encoding self._muted = False - self._pwstate = STATE_OFF + self._pwstate = MediaPlayerState.OFF self._current_source = None with self.projector() as projector: if not self._name: @@ -107,30 +101,30 @@ class PjLinkDevice(MediaPlayerEntity): projector.authenticate(self._password) return projector - def update(self): + def update(self) -> None: """Get the latest state from the device.""" with self.projector() as projector: try: pwstate = projector.get_power() if pwstate in ("on", "warm-up"): - self._pwstate = STATE_ON + self._pwstate = MediaPlayerState.ON self._muted = projector.get_mute()[1] self._current_source = format_input_source(*projector.get_input()) else: - self._pwstate = STATE_OFF + self._pwstate = MediaPlayerState.OFF self._muted = False self._current_source = None except KeyError as err: if str(err) == "'OK'": - self._pwstate = STATE_OFF + self._pwstate = MediaPlayerState.OFF self._muted = False self._current_source = None else: raise except ProjectorError as err: if str(err) == "unavailable time": - self._pwstate = STATE_OFF + self._pwstate = MediaPlayerState.OFF self._muted = False self._current_source = None else: @@ -161,22 +155,22 @@ class PjLinkDevice(MediaPlayerEntity): """Return all available input sources.""" return self._source_list - def turn_off(self): + def turn_off(self) -> None: """Turn projector off.""" with self.projector() as projector: projector.set_power("off") - def turn_on(self): + def turn_on(self) -> None: """Turn projector on.""" with self.projector() as projector: projector.set_power("on") - def mute_volume(self, mute): + def mute_volume(self, mute: bool) -> None: """Mute (true) of unmute (false) media player.""" with self.projector() as projector: projector.set_mute(MUTE_AUDIO, mute) - def select_source(self, source): + def select_source(self, source: str) -> None: """Set the input source.""" source = self._source_name_mapping[source] with self.projector() as projector: diff --git a/homeassistant/components/plaato/translations/pt.json b/homeassistant/components/plaato/translations/pt.json index 3039abbcafb..7e145c2f063 100644 --- a/homeassistant/components/plaato/translations/pt.json +++ b/homeassistant/components/plaato/translations/pt.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Conta j\u00e1 configurada", + "cloud_not_connected": "N\u00e3o ligado ao Home Assistant Cloud.", "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel.", "webhook_not_internet_accessible": "O seu Home Assistant necessita estar acess\u00edvel a partir da Internet para receber mensagens do tipo webhook." }, diff --git a/homeassistant/components/plant/__init__.py b/homeassistant/components/plant/__init__.py index 0d95ccbc300..69f440b6859 100644 --- a/homeassistant/components/plant/__init__.py +++ b/homeassistant/components/plant/__init__.py @@ -111,7 +111,7 @@ CONFIG_SCHEMA = vol.Schema({DOMAIN: {cv.string: PLANT_SCHEMA}}, extra=vol.ALLOW_ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Plant component.""" - component = EntityComponent(_LOGGER, DOMAIN, hass) + component = EntityComponent[Plant](_LOGGER, DOMAIN, hass) entities = [] for plant_name, plant_config in config[DOMAIN].items(): diff --git a/homeassistant/components/plex/__init__.py b/homeassistant/components/plex/__init__.py index 9ff8bcf7b54..20f14de56c9 100644 --- a/homeassistant/components/plex/__init__.py +++ b/homeassistant/components/plex/__init__.py @@ -34,7 +34,7 @@ from .const import ( CONF_SERVER, CONF_SERVER_IDENTIFIER, DISPATCHERS, - DOMAIN as PLEX_DOMAIN, + DOMAIN, GDM_DEBOUNCER, GDM_SCANNER, PLATFORMS, @@ -62,7 +62,7 @@ def is_plex_media_id(media_content_id): async def async_browse_media(hass, media_content_type, media_content_id, platform=None): """Browse Plex media.""" - plex_server = next(iter(hass.data[PLEX_DOMAIN][SERVERS].values()), None) + plex_server = next(iter(hass.data[DOMAIN][SERVERS].values()), None) if not plex_server: raise BrowseError("No Plex servers available") is_internal = is_internal_request(hass) @@ -81,7 +81,7 @@ async def async_browse_media(hass, media_content_type, media_content_id, platfor async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Plex component.""" hass.data.setdefault( - PLEX_DOMAIN, + DOMAIN, {SERVERS: {}, DISPATCHERS: {}, WEBSOCKETS: {}, PLATFORMS_COMPLETED: {}}, ) @@ -89,13 +89,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: hass.http.register_view(PlexImageView()) - gdm = hass.data[PLEX_DOMAIN][GDM_SCANNER] = GDM() + gdm = hass.data[DOMAIN][GDM_SCANNER] = GDM() def gdm_scan(): _LOGGER.debug("Scanning for GDM clients") gdm.scan(scan_for_clients=True) - hass.data[PLEX_DOMAIN][GDM_DEBOUNCER] = Debouncer[None]( + hass.data[DOMAIN][GDM_DEBOUNCER] = Debouncer[None]( hass, _LOGGER, cooldown=10, @@ -160,8 +160,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: "Connected to: %s (%s)", plex_server.friendly_name, plex_server.url_in_use ) server_id = plex_server.machine_identifier - hass.data[PLEX_DOMAIN][SERVERS][server_id] = plex_server - hass.data[PLEX_DOMAIN][PLATFORMS_COMPLETED][server_id] = set() + hass.data[DOMAIN][SERVERS][server_id] = plex_server + hass.data[DOMAIN][PLATFORMS_COMPLETED][server_id] = set() entry.add_update_listener(async_options_updated) @@ -170,8 +170,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id), plex_server.async_update_platforms, ) - hass.data[PLEX_DOMAIN][DISPATCHERS].setdefault(server_id, []) - hass.data[PLEX_DOMAIN][DISPATCHERS][server_id].append(unsub) + hass.data[DOMAIN][DISPATCHERS].setdefault(server_id, []) + hass.data[DOMAIN][DISPATCHERS][server_id].append(unsub) @callback def plex_websocket_callback(msgtype, data, error): @@ -213,11 +213,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: session=session, verify_ssl=verify_ssl, ) - hass.data[PLEX_DOMAIN][WEBSOCKETS][server_id] = websocket + hass.data[DOMAIN][WEBSOCKETS][server_id] = websocket def start_websocket_session(platform): - hass.data[PLEX_DOMAIN][PLATFORMS_COMPLETED][server_id].add(platform) - if hass.data[PLEX_DOMAIN][PLATFORMS_COMPLETED][server_id] == PLATFORMS: + hass.data[DOMAIN][PLATFORMS_COMPLETED][server_id].add(platform) + if hass.data[DOMAIN][PLATFORMS_COMPLETED][server_id] == PLATFORMS: hass.loop.create_task(websocket.listen()) def close_websocket_session(_): @@ -226,7 +226,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: unsub = hass.bus.async_listen_once( EVENT_HOMEASSISTANT_STOP, close_websocket_session ) - hass.data[PLEX_DOMAIN][DISPATCHERS][server_id].append(unsub) + hass.data[DOMAIN][DISPATCHERS][server_id].append(unsub) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) @@ -263,16 +263,16 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" server_id = entry.data[CONF_SERVER_IDENTIFIER] - websocket = hass.data[PLEX_DOMAIN][WEBSOCKETS].pop(server_id) + websocket = hass.data[DOMAIN][WEBSOCKETS].pop(server_id) websocket.close() - dispatchers = hass.data[PLEX_DOMAIN][DISPATCHERS].pop(server_id) + dispatchers = hass.data[DOMAIN][DISPATCHERS].pop(server_id) for unsub in dispatchers: unsub() unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - hass.data[PLEX_DOMAIN][SERVERS].pop(server_id) + hass.data[DOMAIN][SERVERS].pop(server_id) return unload_ok @@ -282,8 +282,8 @@ async def async_options_updated(hass: HomeAssistant, entry: ConfigEntry) -> None server_id = entry.data[CONF_SERVER_IDENTIFIER] # Guard incomplete setup during reauth flows - if server_id in hass.data[PLEX_DOMAIN][SERVERS]: - hass.data[PLEX_DOMAIN][SERVERS][server_id].options = entry.options + if server_id in hass.data[DOMAIN][SERVERS]: + hass.data[DOMAIN][SERVERS][server_id].options = entry.options @callback diff --git a/homeassistant/components/plex/cast.py b/homeassistant/components/plex/cast.py index c85b6ee3a78..7dc112b72de 100644 --- a/homeassistant/components/plex/cast.py +++ b/homeassistant/components/plex/cast.py @@ -4,9 +4,8 @@ from __future__ import annotations from pychromecast import Chromecast from pychromecast.controllers.plex import PlexController -from homeassistant.components.cast.const import DOMAIN as CAST_DOMAIN -from homeassistant.components.media_player import BrowseMedia -from homeassistant.components.media_player.const import MEDIA_CLASS_APP +from homeassistant.components.cast import DOMAIN as CAST_DOMAIN +from homeassistant.components.media_player import BrowseMedia, MediaClass, MediaType from homeassistant.core import HomeAssistant from . import async_browse_media as async_browse_plex_media, is_plex_media_id @@ -20,7 +19,7 @@ async def async_get_media_browser_root_object( return [ BrowseMedia( title="Plex", - media_class=MEDIA_CLASS_APP, + media_class=MediaClass.APP, media_content_id="", media_content_type="plex", thumbnail="https://brands.home-assistant.io/_/plex/logo.png", @@ -32,7 +31,7 @@ async def async_get_media_browser_root_object( async def async_browse_media( hass: HomeAssistant, - media_content_type: str, + media_content_type: MediaType | str, media_content_id: str, cast_type: str, ) -> BrowseMedia | None: @@ -61,7 +60,7 @@ async def async_play_media( hass: HomeAssistant, cast_entity_id: str, chromecast: Chromecast, - media_type: str, + media_type: MediaType | str, media_id: str, ) -> bool: """Play media.""" diff --git a/homeassistant/components/plex/media_browser.py b/homeassistant/components/plex/media_browser.py index d65c01410c7..95ad3f39c70 100644 --- a/homeassistant/components/plex/media_browser.py +++ b/homeassistant/components/plex/media_browser.py @@ -3,20 +3,7 @@ from __future__ import annotations from yarl import URL -from homeassistant.components.media_player import BrowseMedia -from homeassistant.components.media_player.const import ( - MEDIA_CLASS_ALBUM, - MEDIA_CLASS_ARTIST, - MEDIA_CLASS_DIRECTORY, - MEDIA_CLASS_EPISODE, - MEDIA_CLASS_MOVIE, - MEDIA_CLASS_PLAYLIST, - MEDIA_CLASS_SEASON, - MEDIA_CLASS_TRACK, - MEDIA_CLASS_TV_SHOW, - MEDIA_CLASS_VIDEO, -) -from homeassistant.components.media_player.errors import BrowseError +from homeassistant.components.media_player import BrowseError, BrowseMedia, MediaClass from .const import DOMAIN, SERVERS from .errors import MediaNotFound @@ -29,18 +16,18 @@ class UnknownMediaType(BrowseError): EXPANDABLES = ["album", "artist", "playlist", "season", "show"] ITEM_TYPE_MEDIA_CLASS = { - "album": MEDIA_CLASS_ALBUM, - "artist": MEDIA_CLASS_ARTIST, - "clip": MEDIA_CLASS_VIDEO, - "episode": MEDIA_CLASS_EPISODE, - "mixed": MEDIA_CLASS_DIRECTORY, - "movie": MEDIA_CLASS_MOVIE, - "playlist": MEDIA_CLASS_PLAYLIST, - "season": MEDIA_CLASS_SEASON, - "show": MEDIA_CLASS_TV_SHOW, - "station": MEDIA_CLASS_ARTIST, - "track": MEDIA_CLASS_TRACK, - "video": MEDIA_CLASS_VIDEO, + "album": MediaClass.ALBUM, + "artist": MediaClass.ARTIST, + "clip": MediaClass.VIDEO, + "episode": MediaClass.EPISODE, + "mixed": MediaClass.DIRECTORY, + "movie": MediaClass.MOVIE, + "playlist": MediaClass.PLAYLIST, + "season": MediaClass.SEASON, + "show": MediaClass.TV_SHOW, + "station": MediaClass.ARTIST, + "track": MediaClass.TRACK, + "video": MediaClass.VIDEO, } @@ -99,13 +86,13 @@ def browse_media( # noqa: C901 """Create response payload to describe libraries of the Plex server.""" server_info = BrowseMedia( title=plex_server.friendly_name, - media_class=MEDIA_CLASS_DIRECTORY, + media_class=MediaClass.DIRECTORY, media_content_id=generate_plex_uri(server_id, "server"), media_content_type="server", can_play=False, can_expand=True, children=[], - children_media_class=MEDIA_CLASS_DIRECTORY, + children_media_class=MediaClass.DIRECTORY, thumbnail="https://brands.home-assistant.io/_/plex/logo.png", ) if platform != "sonos": @@ -136,7 +123,7 @@ def browse_media( # noqa: C901 """Create response payload for all available playlists.""" playlists_info = { "title": "Playlists", - "media_class": MEDIA_CLASS_DIRECTORY, + "media_class": MediaClass.DIRECTORY, "media_content_id": generate_plex_uri(server_id, "all"), "media_content_type": "playlists", "can_play": False, @@ -151,7 +138,7 @@ def browse_media( # noqa: C901 except UnknownMediaType: continue response = BrowseMedia(**playlists_info) - response.children_media_class = MEDIA_CLASS_PLAYLIST + response.children_media_class = MediaClass.PLAYLIST return response def build_item_response(payload): @@ -197,7 +184,7 @@ def browse_media( # noqa: C901 raise UnknownMediaType(f"Unknown type received: {hub.type}") from err payload = { "title": hub.title, - "media_class": MEDIA_CLASS_DIRECTORY, + "media_class": MediaClass.DIRECTORY, "media_content_id": generate_plex_uri(server_id, media_content_id), "media_content_type": "hub", "can_play": False, @@ -223,7 +210,7 @@ def browse_media( # noqa: C901 if special_folder: if media_content_type == "server": library_or_section = plex_server.library - children_media_class = MEDIA_CLASS_DIRECTORY + children_media_class = MediaClass.DIRECTORY title = plex_server.friendly_name elif media_content_type == "library": library_or_section = plex_server.library.sectionByID(int(media_content_id)) @@ -241,7 +228,7 @@ def browse_media( # noqa: C901 payload = { "title": title, - "media_class": MEDIA_CLASS_DIRECTORY, + "media_class": MediaClass.DIRECTORY, "media_content_id": generate_plex_uri( server_id, f"{media_content_id}/{special_folder}" ), @@ -323,7 +310,7 @@ def root_payload(hass, is_internal, platform=None): return BrowseMedia( title="Plex", - media_class=MEDIA_CLASS_DIRECTORY, + media_class=MediaClass.DIRECTORY, media_content_id="", media_content_type="plex_root", can_play=False, @@ -341,7 +328,7 @@ def library_section_payload(section): server_id = section._server.machineIdentifier # pylint: disable=protected-access return BrowseMedia( title=section.title, - media_class=MEDIA_CLASS_DIRECTORY, + media_class=MediaClass.DIRECTORY, media_content_id=generate_plex_uri(server_id, section.key), media_content_type="library", can_play=False, @@ -374,7 +361,7 @@ def hub_payload(hub): server_id = hub._server.machineIdentifier # pylint: disable=protected-access payload = { "title": hub.title, - "media_class": MEDIA_CLASS_DIRECTORY, + "media_class": MediaClass.DIRECTORY, "media_content_id": generate_plex_uri(server_id, media_content_id), "media_content_type": "hub", "can_play": False, diff --git a/homeassistant/components/plex/media_player.py b/homeassistant/components/plex/media_player.py index 7d21fff3afe..84e0f084210 100644 --- a/homeassistant/components/plex/media_player.py +++ b/homeassistant/components/plex/media_player.py @@ -4,7 +4,7 @@ from __future__ import annotations from collections.abc import Callable from functools import wraps import logging -from typing import TypeVar +from typing import Any, TypeVar import plexapi.exceptions import requests.exceptions @@ -12,12 +12,13 @@ from typing_extensions import Concatenate, ParamSpec from homeassistant.components.media_player import ( DOMAIN as MP_DOMAIN, + BrowseMedia, MediaPlayerEntity, MediaPlayerEntityFeature, + MediaPlayerState, + MediaType, ) -from homeassistant.components.media_player.const import MEDIA_TYPE_MUSIC from homeassistant.config_entries import ConfigEntry -from homeassistant.const import STATE_IDLE, STATE_PAUSED, STATE_PLAYING from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er @@ -34,7 +35,7 @@ from .const import ( COMMON_PLAYERS, CONF_SERVER_IDENTIFIER, DISPATCHERS, - DOMAIN as PLEX_DOMAIN, + DOMAIN, NAME_FORMAT, PLEX_NEW_MP_SIGNAL, PLEX_UPDATE_MEDIA_PLAYER_SESSION_SIGNAL, @@ -85,7 +86,7 @@ async def async_setup_entry( unsub = async_dispatcher_connect( hass, PLEX_NEW_MP_SIGNAL.format(server_id), async_new_media_players ) - hass.data[PLEX_DOMAIN][DISPATCHERS][server_id].append(unsub) + hass.data[DOMAIN][DISPATCHERS][server_id].append(unsub) _LOGGER.debug("New entity listener created") @@ -94,14 +95,14 @@ def _async_add_entities(hass, registry, async_add_entities, server_id, new_entit """Set up Plex media_player entities.""" _LOGGER.debug("New entities: %s", new_entities) entities = [] - plexserver = hass.data[PLEX_DOMAIN][SERVERS][server_id] + plexserver = hass.data[DOMAIN][SERVERS][server_id] for entity_params in new_entities: plex_mp = PlexMediaPlayer(plexserver, **entity_params) entities.append(plex_mp) # Migration to per-server unique_ids old_entity_id = registry.async_get_entity_id( - MP_DOMAIN, PLEX_DOMAIN, plex_mp.machine_identifier + MP_DOMAIN, DOMAIN, plex_mp.machine_identifier ) if old_entity_id is not None: new_unique_id = f"{server_id}:{plex_mp.machine_identifier}" @@ -139,7 +140,7 @@ class PlexMediaPlayer(MediaPlayerEntity): self._attr_available = False self._attr_should_poll = False - self._attr_state = STATE_IDLE + self._attr_state = MediaPlayerState.IDLE self._attr_unique_id = ( f"{self.plex_server.machine_identifier}:{self.machine_identifier}" ) @@ -147,7 +148,7 @@ class PlexMediaPlayer(MediaPlayerEntity): # Initializes other attributes self.session = session - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Run when about to be added to hass.""" _LOGGER.debug("Added %s [%s]", self.entity_id, self.unique_id) self.async_on_remove( @@ -228,7 +229,7 @@ class PlexMediaPlayer(MediaPlayerEntity): def force_idle(self): """Force client to idle.""" - self._attr_state = STATE_IDLE + self._attr_state = MediaPlayerState.IDLE if self.player_source == "session": self.device = None self.session_device = None @@ -246,9 +247,9 @@ class PlexMediaPlayer(MediaPlayerEntity): self.session_device = self.session.player self.update_state(self.session.state) else: - self._attr_state = STATE_IDLE + self._attr_state = MediaPlayerState.IDLE - @property # type: ignore[misc] + @property @needs_session def username(self): """Return the username of the client owner.""" @@ -257,131 +258,131 @@ class PlexMediaPlayer(MediaPlayerEntity): def update_state(self, state): """Set the state of the device, handle session termination.""" if state == "playing": - self._attr_state = STATE_PLAYING + self._attr_state = MediaPlayerState.PLAYING elif state == "paused": - self._attr_state = STATE_PAUSED + self._attr_state = MediaPlayerState.PAUSED elif state == "stopped": self.session = None self.force_idle() else: - self._attr_state = STATE_IDLE + self._attr_state = MediaPlayerState.IDLE @property def _is_player_active(self): """Report if the client is playing media.""" - return self.state in (STATE_PLAYING, STATE_PAUSED) + return self.state in {MediaPlayerState.PLAYING, MediaPlayerState.PAUSED} @property def _active_media_plexapi_type(self): """Get the active media type required by PlexAPI commands.""" - if self.media_content_type is MEDIA_TYPE_MUSIC: + if self.media_content_type == MediaType.MUSIC: return "music" return "video" - @property # type: ignore[misc] + @property @needs_session def session_key(self): """Return current session key.""" return self.session.sessionKey - @property # type: ignore[misc] + @property @needs_session def media_library_title(self): """Return the library name of playing media.""" return self.session.media_library_title - @property # type: ignore[misc] + @property @needs_session def media_content_id(self): """Return the content ID of current playing media.""" return self.session.media_content_id - @property # type: ignore[misc] + @property @needs_session def media_content_type(self): """Return the content type of current playing media.""" return self.session.media_content_type - @property # type: ignore[misc] + @property @needs_session def media_content_rating(self): """Return the content rating of current playing media.""" return self.session.media_content_rating - @property # type: ignore[misc] + @property @needs_session def media_artist(self): """Return the artist of current playing media, music track only.""" return self.session.media_artist - @property # type: ignore[misc] + @property @needs_session def media_album_name(self): """Return the album name of current playing media, music track only.""" return self.session.media_album_name - @property # type: ignore[misc] + @property @needs_session def media_album_artist(self): """Return the album artist of current playing media, music only.""" return self.session.media_album_artist - @property # type: ignore[misc] + @property @needs_session def media_track(self): """Return the track number of current playing media, music only.""" return self.session.media_track - @property # type: ignore[misc] + @property @needs_session def media_duration(self): """Return the duration of current playing media in seconds.""" return self.session.media_duration - @property # type: ignore[misc] + @property @needs_session def media_position(self): """Return the duration of current playing media in seconds.""" return self.session.media_position - @property # type: ignore[misc] + @property @needs_session def media_position_updated_at(self): """When was the position of the current playing media valid.""" return self.session.media_position_updated_at - @property # type: ignore[misc] + @property @needs_session def media_image_url(self): """Return the image URL of current playing media.""" return self.session.media_image_url - @property # type: ignore[misc] + @property @needs_session def media_summary(self): """Return the summary of current playing media.""" return self.session.media_summary - @property # type: ignore[misc] + @property @needs_session def media_title(self): """Return the title of current playing media.""" return self.session.media_title - @property # type: ignore[misc] + @property @needs_session def media_season(self): """Return the season of current playing media (TV Show only).""" return self.session.media_season - @property # type: ignore[misc] + @property @needs_session def media_series_title(self): """Return the title of the series of current playing media.""" return self.session.media_series_title - @property # type: ignore[misc] + @property @needs_session def media_episode(self): """Return the episode of current playing media (TV Show only).""" @@ -408,7 +409,7 @@ class PlexMediaPlayer(MediaPlayerEntity): MediaPlayerEntityFeature.BROWSE_MEDIA | MediaPlayerEntityFeature.PLAY_MEDIA ) - def set_volume_level(self, volume): + def set_volume_level(self, volume: float) -> None: """Set volume level, range 0..1.""" if self.device and "playback" in self._device_protocol_capabilities: self.device.setVolume(int(volume * 100), self._active_media_plexapi_type) @@ -432,7 +433,7 @@ class PlexMediaPlayer(MediaPlayerEntity): return self._volume_muted return None - def mute_volume(self, mute): + def mute_volume(self, mute: bool) -> None: """Mute the volume. Since we can't actually mute, we'll: @@ -449,37 +450,37 @@ class PlexMediaPlayer(MediaPlayerEntity): else: self.set_volume_level(self._previous_volume_level) - def media_play(self): + def media_play(self) -> None: """Send play command.""" if self.device and "playback" in self._device_protocol_capabilities: self.device.play(self._active_media_plexapi_type) - def media_pause(self): + def media_pause(self) -> None: """Send pause command.""" if self.device and "playback" in self._device_protocol_capabilities: self.device.pause(self._active_media_plexapi_type) - def media_stop(self): + def media_stop(self) -> None: """Send stop command.""" if self.device and "playback" in self._device_protocol_capabilities: self.device.stop(self._active_media_plexapi_type) - def media_seek(self, position): + def media_seek(self, position: float) -> None: """Send the seek command.""" if self.device and "playback" in self._device_protocol_capabilities: self.device.seekTo(position * 1000, self._active_media_plexapi_type) - def media_next_track(self): + def media_next_track(self) -> None: """Send next track command.""" if self.device and "playback" in self._device_protocol_capabilities: self.device.skipNext(self._active_media_plexapi_type) - def media_previous_track(self): + def media_previous_track(self) -> None: """Send previous track command.""" if self.device and "playback" in self._device_protocol_capabilities: self.device.skipPrevious(self._active_media_plexapi_type) - def play_media(self, media_type, media_id, **kwargs): + def play_media(self, media_type: str, media_id: str, **kwargs: Any) -> None: """Play a piece of media.""" if not (self.device and "playback" in self._device_protocol_capabilities): raise HomeAssistantError( @@ -522,7 +523,7 @@ class PlexMediaPlayer(MediaPlayerEntity): if self.device_product in TRANSIENT_DEVICE_MODELS: return DeviceInfo( - identifiers={(PLEX_DOMAIN, "plex.tv-clients")}, + identifiers={(DOMAIN, "plex.tv-clients")}, name="Plex Client Service", manufacturer="Plex", model="Plex Clients", @@ -530,15 +531,17 @@ class PlexMediaPlayer(MediaPlayerEntity): ) return DeviceInfo( - identifiers={(PLEX_DOMAIN, self.machine_identifier)}, + identifiers={(DOMAIN, self.machine_identifier)}, manufacturer=self.device_platform or "Plex", model=self.device_product or self.device_make, name=self.name, sw_version=self.device_version, - via_device=(PLEX_DOMAIN, self.plex_server.machine_identifier), + via_device=(DOMAIN, self.plex_server.machine_identifier), ) - 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.""" is_internal = is_internal_request(self.hass) return await self.hass.async_add_executor_job( diff --git a/homeassistant/components/plex/models.py b/homeassistant/components/plex/models.py index 48eee9d988d..d834012d287 100644 --- a/homeassistant/components/plex/models.py +++ b/homeassistant/components/plex/models.py @@ -1,12 +1,7 @@ """Models to represent various Plex objects used in the integration.""" import logging -from homeassistant.components.media_player.const import ( - MEDIA_TYPE_MOVIE, - MEDIA_TYPE_MUSIC, - MEDIA_TYPE_TVSHOW, - MEDIA_TYPE_VIDEO, -) +from homeassistant.components.media_player import MediaType from homeassistant.helpers.template import result_as_boolean from homeassistant.util import dt as dt_util @@ -92,19 +87,19 @@ class PlexSession: ) if media.type == "episode": - self.media_content_type = MEDIA_TYPE_TVSHOW + self.media_content_type = MediaType.TVSHOW self.media_season = media.seasonNumber self.media_series_title = media.grandparentTitle if media.index is not None: self.media_episode = media.index self.sensor_title = f"{self.media_series_title} - {media.seasonEpisode} - {self.media_title}" elif media.type == "movie": - self.media_content_type = MEDIA_TYPE_MOVIE + self.media_content_type = MediaType.MOVIE if media.year is not None and media.title is not None: self.media_title += f" ({media.year!s})" self.sensor_title = self.media_title elif media.type == "track": - self.media_content_type = MEDIA_TYPE_MUSIC + self.media_content_type = MediaType.MUSIC self.media_album_name = media.parentTitle self.media_album_artist = media.grandparentTitle self.media_track = media.index @@ -113,7 +108,7 @@ class PlexSession: f"{self.media_artist} - {self.media_album_name} - {self.media_title}" ) elif media.type == "clip": - self.media_content_type = MEDIA_TYPE_VIDEO + self.media_content_type = MediaType.VIDEO self.sensor_title = media.title else: self.sensor_title = "Unknown" diff --git a/homeassistant/components/plex/sensor.py b/homeassistant/components/plex/sensor.py index 20e4b4ffb51..f4a2ac6e03a 100644 --- a/homeassistant/components/plex/sensor.py +++ b/homeassistant/components/plex/sensor.py @@ -16,7 +16,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( CONF_SERVER_IDENTIFIER, - DOMAIN as PLEX_DOMAIN, + DOMAIN, NAME_FORMAT, PLEX_UPDATE_LIBRARY_SIGNAL, PLEX_UPDATE_SENSOR_SIGNAL, @@ -57,7 +57,7 @@ async def async_setup_entry( ) -> None: """Set up Plex sensor from a config entry.""" server_id = config_entry.data[CONF_SERVER_IDENTIFIER] - plexserver = hass.data[PLEX_DOMAIN][SERVERS][server_id] + plexserver = hass.data[DOMAIN][SERVERS][server_id] sensors = [PlexSensor(hass, plexserver)] def create_library_sensors(): @@ -89,7 +89,7 @@ class PlexSensor(SensorEntity): function=self._async_refresh_sensor, ).async_call - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Run when about to be added to hass.""" server_id = self._server.machine_identifier self.async_on_remove( @@ -100,7 +100,7 @@ class PlexSensor(SensorEntity): ) ) - async def _async_refresh_sensor(self): + async def _async_refresh_sensor(self) -> None: """Set instance object and trigger an entity state update.""" _LOGGER.debug("Refreshing sensor [%s]", self.unique_id) self._attr_native_value = len(self._server.sensor_attributes) @@ -118,7 +118,7 @@ class PlexSensor(SensorEntity): return None return DeviceInfo( - identifiers={(PLEX_DOMAIN, self._server.machine_identifier)}, + identifiers={(DOMAIN, self._server.machine_identifier)}, manufacturer="Plex", model="Plex Media Server", name=self._server.friendly_name, @@ -147,7 +147,7 @@ class PlexLibrarySectionSensor(SensorEntity): self._attr_unique_id = f"library-{self.server_id}-{plex_library_section.uuid}" self._attr_native_unit_of_measurement = "Items" - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Run when about to be added to hass.""" self.async_on_remove( async_dispatcher_connect( @@ -158,7 +158,7 @@ class PlexLibrarySectionSensor(SensorEntity): ) await self.async_refresh_sensor() - async def async_refresh_sensor(self): + async def async_refresh_sensor(self) -> None: """Update state and attributes for the library sensor.""" _LOGGER.debug("Refreshing library sensor for '%s'", self.name) try: @@ -209,7 +209,7 @@ class PlexLibrarySectionSensor(SensorEntity): return None return DeviceInfo( - identifiers={(PLEX_DOMAIN, self.server_id)}, + identifiers={(DOMAIN, self.server_id)}, manufacturer="Plex", model="Plex Media Server", name=self.server_name, diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py index 058e8abbecd..8bcb0192cd2 100644 --- a/homeassistant/components/plex/server.py +++ b/homeassistant/components/plex/server.py @@ -12,8 +12,7 @@ import plexapi.server from requests import Session import requests.exceptions -from homeassistant.components.media_player import DOMAIN as MP_DOMAIN -from homeassistant.components.media_player.const import MEDIA_TYPE_PLAYLIST +from homeassistant.components.media_player import DOMAIN as MP_DOMAIN, MediaType from homeassistant.const import CONF_CLIENT_ID, CONF_TOKEN, CONF_URL, CONF_VERIFY_SSL from homeassistant.core import callback from homeassistant.helpers.debounce import Debouncer @@ -627,7 +626,7 @@ class PlexServer: except NotFound as err: raise MediaNotFound(f"Media for key {key} not found") from err - if media_type == MEDIA_TYPE_PLAYLIST: + if media_type == MediaType.PLAYLIST: try: playlist_name = kwargs["playlist_name"] return self.playlist(playlist_name) diff --git a/homeassistant/components/plex/translations/es.json b/homeassistant/components/plex/translations/es.json index 14fe1b840a5..8074612c8b5 100644 --- a/homeassistant/components/plex/translations/es.json +++ b/homeassistant/components/plex/translations/es.json @@ -4,7 +4,7 @@ "all_configured": "Todos los servidores vinculados ya configurados", "already_configured": "Este servidor Plex ya est\u00e1 configurado", "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente", "token_request_timeout": "Tiempo de espera agotado para la obtenci\u00f3n del token", "unknown": "Error inesperado" }, diff --git a/homeassistant/components/plex/view.py b/homeassistant/components/plex/view.py index 5780bd3c46b..a2c31f17eb1 100644 --- a/homeassistant/components/plex/view.py +++ b/homeassistant/components/plex/view.py @@ -11,7 +11,7 @@ from aiohttp.typedefs import LooseHeaders from homeassistant.components.http import KEY_AUTHENTICATED, HomeAssistantView from homeassistant.components.media_player import async_fetch_image -from .const import DOMAIN as PLEX_DOMAIN, SERVERS +from .const import DOMAIN, SERVERS _LOGGER = logging.getLogger(__name__) @@ -33,7 +33,7 @@ class PlexImageView(HomeAssistantView): return web.Response(status=HTTPStatus.UNAUTHORIZED) hass = request.app["hass"] - if (server := hass.data[PLEX_DOMAIN][SERVERS].get(server_id)) is None: + if (server := hass.data[DOMAIN][SERVERS].get(server_id)) is None: return web.Response(status=HTTPStatus.NOT_FOUND) if (image_url := server.thumbnail_cache.get(media_content_id)) is None: diff --git a/homeassistant/components/plugwise/climate.py b/homeassistant/components/plugwise/climate.py index 9729ad745fd..84dc4576700 100644 --- a/homeassistant/components/plugwise/climate.py +++ b/homeassistant/components/plugwise/climate.py @@ -4,8 +4,8 @@ from __future__ import annotations from collections.abc import Mapping from typing import Any -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( + ClimateEntity, ClimateEntityFeature, HVACAction, HVACMode, @@ -13,9 +13,10 @@ from homeassistant.components.climate.const import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP, DOMAIN, THERMOSTAT_CLASSES +from .const import DOMAIN, MASTER_THERMOSTATS from .coordinator import PlugwiseDataUpdateCoordinator from .entity import PlugwiseEntity from .util import plugwise_command @@ -31,7 +32,7 @@ async def async_setup_entry( async_add_entities( PlugwiseClimateEntity(coordinator, device_id) for device_id, device in coordinator.data.devices.items() - if device["dev_class"] in THERMOSTAT_CLASSES + if device["dev_class"] in MASTER_THERMOSTATS ) @@ -59,26 +60,27 @@ class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity): # Determine hvac modes and current hvac mode self._attr_hvac_modes = [HVACMode.HEAT] - if self.coordinator.data.gateway.get("cooling_present"): + if self.coordinator.data.gateway["cooling_present"]: self._attr_hvac_modes.append(HVACMode.COOL) - if self.device.get("available_schedules") != ["None"]: + if self.device["available_schedules"] != ["None"]: self._attr_hvac_modes.append(HVACMode.AUTO) - self._attr_min_temp = self.device.get("lower_bound", DEFAULT_MIN_TEMP) - self._attr_max_temp = self.device.get("upper_bound", DEFAULT_MAX_TEMP) - if resolution := self.device.get("resolution"): - # Ensure we don't drop below 0.1 - self._attr_target_temperature_step = max(resolution, 0.1) + self._attr_min_temp = self.device["thermostat"]["lower_bound"] + self._attr_max_temp = self.device["thermostat"]["upper_bound"] + # Ensure we don't drop below 0.1 + self._attr_target_temperature_step = max( + self.device["thermostat"]["resolution"], 0.1 + ) @property - def current_temperature(self) -> float | None: + def current_temperature(self) -> float: """Return the current temperature.""" - return self.device["sensors"].get("temperature") + return self.device["sensors"]["temperature"] @property - def target_temperature(self) -> float | None: + def target_temperature(self) -> float: """Return the temperature we try to reach.""" - return self.device["sensors"].get("setpoint") + return self.device["thermostat"]["setpoint"] @property def hvac_mode(self) -> HVACMode: @@ -88,23 +90,24 @@ class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity): return HVACMode(mode) @property - def hvac_action(self) -> HVACAction: + def hvac_action(self) -> HVACAction | None: """Return the current running hvac operation if supported.""" # When control_state is present, prefer this data - if "control_state" in self.device: - if self.device.get("control_state") == "cooling": - return HVACAction.COOLING - # Support preheating state as heating, until preheating is added as a separate state - if self.device.get("control_state") in ["heating", "preheating"]: - return HVACAction.HEATING - else: - heater_central_data = self.coordinator.data.devices[ - self.coordinator.data.gateway["heater_id"] - ] - if heater_central_data["binary_sensors"].get("heating_state"): - return HVACAction.HEATING - if heater_central_data["binary_sensors"].get("cooling_state"): - return HVACAction.COOLING + if (control_state := self.device.get("control_state")) == "cooling": + return HVACAction.COOLING + # Support preheating state as heating, until preheating is added as a separate state + if control_state in ["heating", "preheating"]: + return HVACAction.HEATING + if control_state == "off": + return HVACAction.IDLE + + hc_data = self.coordinator.data.devices[ + self.coordinator.data.gateway["heater_id"] + ] + if hc_data["binary_sensors"]["heating_state"]: + return HVACAction.HEATING + if hc_data["binary_sensors"].get("cooling_state"): + return HVACAction.COOLING return HVACAction.IDLE @@ -117,25 +120,29 @@ class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity): def extra_state_attributes(self) -> Mapping[str, Any] | None: """Return entity specific state attributes.""" return { - "available_schemas": self.device.get("available_schedules"), - "selected_schema": self.device.get("selected_schedule"), + "available_schemas": self.device["available_schedules"], + "selected_schema": self.device["selected_schedule"], } @plugwise_command async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" - if ((temperature := kwargs.get(ATTR_TEMPERATURE)) is None) or ( - self._attr_max_temp < temperature < self._attr_min_temp + if ((temperature := kwargs.get(ATTR_TEMPERATURE)) is None) or not ( + self._attr_min_temp <= temperature <= self._attr_max_temp ): - raise ValueError("Invalid temperature requested") + raise ValueError("Invalid temperature change requested") + await self.coordinator.api.set_temperature(self.device["location"], temperature) @plugwise_command async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set the hvac mode.""" + if hvac_mode not in self.hvac_modes: + raise HomeAssistantError("Unsupported hvac_mode") + await self.coordinator.api.set_schedule_state( self.device["location"], - self.device.get("last_used"), + self.device["last_used"], "on" if hvac_mode == HVACMode.AUTO else "off", ) diff --git a/homeassistant/components/plugwise/const.py b/homeassistant/components/plugwise/const.py index d56d9c06ff5..c2d0d75c8a0 100644 --- a/homeassistant/components/plugwise/const.py +++ b/homeassistant/components/plugwise/const.py @@ -48,7 +48,7 @@ DEFAULT_SCAN_INTERVAL: Final[dict[str, timedelta]] = { } DEFAULT_USERNAME: Final = "smile" -THERMOSTAT_CLASSES: Final[list[str]] = [ +MASTER_THERMOSTATS: Final[list[str]] = [ "thermostat", "thermostatic_radiator_valve", "zone_thermometer", diff --git a/homeassistant/components/plugwise/manifest.json b/homeassistant/components/plugwise/manifest.json index bf7fc453f89..9c8ea6f3be7 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.18.7"], + "requirements": ["plugwise==0.21.3"], "codeowners": ["@CoMPaTech", "@bouwew", "@brefra", "@frenck"], "zeroconf": ["_plugwise._tcp.local."], "config_flow": true, diff --git a/homeassistant/components/plugwise/number.py b/homeassistant/components/plugwise/number.py index 380be9111ba..9100e006968 100644 --- a/homeassistant/components/plugwise/number.py +++ b/homeassistant/components/plugwise/number.py @@ -27,7 +27,11 @@ from .entity import PlugwiseEntity class PlugwiseEntityDescriptionMixin: """Mixin values for Plugwse entities.""" - command: Callable[[Smile, float], Awaitable[None]] + command: Callable[[Smile, str, float], Awaitable[None]] + native_max_value_key: str + native_min_value_key: str + native_step_key: str + native_value_key: str @dataclass @@ -40,11 +44,15 @@ class PlugwiseNumberEntityDescription( NUMBER_TYPES = ( PlugwiseNumberEntityDescription( key="maximum_boiler_temperature", - command=lambda api, value: api.set_max_boiler_temperature(value), + command=lambda api, number, value: api.set_number_setpoint(number, value), device_class=NumberDeviceClass.TEMPERATURE, name="Maximum boiler temperature setpoint", entity_category=EntityCategory.CONFIG, + native_max_value_key="upper_bound", + native_min_value_key="lower_bound", + native_step_key="resolution", native_unit_of_measurement=TEMP_CELSIUS, + native_value_key="setpoint", ), ) @@ -91,24 +99,37 @@ class PlugwiseNumberEntity(PlugwiseEntity, NumberEntity): @property def native_step(self) -> float: """Return the setpoint step value.""" - return max(self.device["resolution"], 1) + return max( + self.device[self.entity_description.key][ + self.entity_description.native_step_key + ], + 1, + ) @property def native_value(self) -> float: """Return the present setpoint value.""" - return self.device[self.entity_description.key] + return self.device[self.entity_description.key][ + self.entity_description.native_value_key + ] @property def native_min_value(self) -> float: """Return the setpoint min. value.""" - return self.device["lower_bound"] + return self.device[self.entity_description.key][ + self.entity_description.native_min_value_key + ] @property def native_max_value(self) -> float: """Return the setpoint max. value.""" - return self.device["upper_bound"] + return self.device[self.entity_description.key][ + self.entity_description.native_max_value_key + ] async def async_set_native_value(self, value: float) -> None: """Change to the new setpoint value.""" - await self.entity_description.command(self.coordinator.api, value) + await self.entity_description.command( + self.coordinator.api, self.entity_description.key, value + ) await self.coordinator.async_request_refresh() diff --git a/homeassistant/components/plugwise/select.py b/homeassistant/components/plugwise/select.py index 7afe76e1a8a..989f56adcf3 100644 --- a/homeassistant/components/plugwise/select.py +++ b/homeassistant/components/plugwise/select.py @@ -24,8 +24,8 @@ class PlugwiseSelectDescriptionMixin: """Mixin values for Plugwise Select entities.""" command: Callable[[Smile, str, str], Awaitable[Any]] - current_option: str - options: str + current_option_key: str + options_key: str @dataclass @@ -41,8 +41,8 @@ SELECT_TYPES = ( name="Thermostat schedule", icon="mdi:calendar-clock", command=lambda api, loc, opt: api.set_schedule_state(loc, opt, STATE_ON), - current_option="selected_schedule", - options="available_schedules", + current_option_key="selected_schedule", + options_key="available_schedules", ), PlugwiseSelectEntityDescription( key="select_regulation_mode", @@ -50,8 +50,8 @@ SELECT_TYPES = ( icon="mdi:hvac", entity_category=EntityCategory.CONFIG, command=lambda api, loc, opt: api.set_regulation_mode(opt), - current_option="regulation_mode", - options="regulation_modes", + current_option_key="regulation_mode", + options_key="regulation_modes", ), ) @@ -69,7 +69,10 @@ async def async_setup_entry( entities: list[PlugwiseSelectEntity] = [] for device_id, device in coordinator.data.devices.items(): for description in SELECT_TYPES: - if description.options in device and len(device[description.options]) > 1: + if ( + description.options_key in device + and len(device[description.options_key]) > 1 + ): entities.append( PlugwiseSelectEntity(coordinator, device_id, description) ) @@ -96,12 +99,12 @@ class PlugwiseSelectEntity(PlugwiseEntity, SelectEntity): @property def current_option(self) -> str: """Return the selected entity option to represent the entity state.""" - return self.device[self.entity_description.current_option] + return self.device[self.entity_description.current_option_key] @property def options(self) -> list[str]: """Return the selectable entity options.""" - return self.device[self.entity_description.options] + return self.device[self.entity_description.options_key] async def async_select_option(self, option: str) -> None: """Change to the selected entity option.""" diff --git a/homeassistant/components/plugwise/translations/cs.json b/homeassistant/components/plugwise/translations/cs.json index c1c193e04a2..a7f5dae7c97 100644 --- a/homeassistant/components/plugwise/translations/cs.json +++ b/homeassistant/components/plugwise/translations/cs.json @@ -12,7 +12,8 @@ "step": { "user": { "data": { - "flow_type": "Typ p\u0159ipojen\u00ed" + "flow_type": "Typ p\u0159ipojen\u00ed", + "host": "IP adresa" }, "description": "Produkt:", "title": "Typ Plugwise" diff --git a/homeassistant/components/plum_lightpad/translations/cs.json b/homeassistant/components/plum_lightpad/translations/cs.json index e530ca166e5..593f2cbfb08 100644 --- a/homeassistant/components/plum_lightpad/translations/cs.json +++ b/homeassistant/components/plum_lightpad/translations/cs.json @@ -4,7 +4,7 @@ "already_configured": "\u00da\u010det je ji\u017e nastaven" }, "error": { - "cannot_connect": "Nelze se p\u0159ipojit" + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" }, "step": { "user": { diff --git a/homeassistant/components/pocketcasts/sensor.py b/homeassistant/components/pocketcasts/sensor.py index 9769ec47f3b..3962ae4c060 100644 --- a/homeassistant/components/pocketcasts/sensor.py +++ b/homeassistant/components/pocketcasts/sensor.py @@ -68,7 +68,7 @@ class PocketCastsSensor(SensorEntity): """Return the icon for the sensor.""" return ICON - def update(self): + def update(self) -> None: """Update sensor values.""" try: self._state = len(self._api.new_releases) diff --git a/homeassistant/components/point/binary_sensor.py b/homeassistant/components/point/binary_sensor.py index a2d2efa9089..e284f2dbe7f 100644 --- a/homeassistant/components/point/binary_sensor.py +++ b/homeassistant/components/point/binary_sensor.py @@ -77,14 +77,14 @@ class MinutPointBinarySensor(MinutPointEntity, BinarySensorEntity): self._async_unsub_hook_dispatcher_connect = None self._events = EVENTS[device_name] - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Call when entity is added to HOme Assistant.""" await super().async_added_to_hass() self._async_unsub_hook_dispatcher_connect = async_dispatcher_connect( self.hass, SIGNAL_WEBHOOK, self._webhook_event ) - async def async_will_remove_from_hass(self): + async def async_will_remove_from_hass(self) -> None: """Disconnect dispatcher listener when removed.""" await super().async_will_remove_from_hass() if self._async_unsub_hook_dispatcher_connect: diff --git a/homeassistant/components/point/translations/fr.json b/homeassistant/components/point/translations/fr.json index 440bd076bef..347313ba8b7 100644 --- a/homeassistant/components/point/translations/fr.json +++ b/homeassistant/components/point/translations/fr.json @@ -11,7 +11,7 @@ "default": "Authentification r\u00e9ussie" }, "error": { - "follow_link": "Veuillez suivre le lien et vous authentifier avant d'appuyer sur Soumettre.", + "follow_link": "Veuillez suivre le lien et vous authentifier avant d'appuyer sur \u00ab\u00a0Valider\u00a0\u00bb", "no_token": "Jeton d'acc\u00e8s non valide" }, "step": { diff --git a/homeassistant/components/powerwall/translations/es.json b/homeassistant/components/powerwall/translations/es.json index 0e34589a260..8d0cf5f8970 100644 --- a/homeassistant/components/powerwall/translations/es.json +++ b/homeassistant/components/powerwall/translations/es.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", "cannot_connect": "No se pudo conectar", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "error": { "cannot_connect": "No se pudo conectar", diff --git a/homeassistant/components/powerwall/translations/ja.json b/homeassistant/components/powerwall/translations/ja.json index be7078143b0..51508f6e3ed 100644 --- a/homeassistant/components/powerwall/translations/ja.json +++ b/homeassistant/components/powerwall/translations/ja.json @@ -21,7 +21,7 @@ "data": { "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" }, - "description": "\u30d1\u30b9\u30ef\u30fc\u30c9\u306f\u901a\u5e38\u3001Backup Gateway\u306e\u30b7\u30ea\u30a2\u30eb\u756a\u53f7\u306e\u6700\u5f8c\u306e5\u6587\u5b57\u3067\u3042\u308a\u3001Tesla\u30a2\u30d7\u30ea\u3067\u898b\u3064\u3051\u308b\u3053\u3068\u304c\u3067\u304d\u307e\u3059\u3002\u307e\u305f\u306f\u3001Backup Gateway2\u306e\u30c9\u30a2\u306e\u5185\u5074\u306b\u3042\u308b\u30d1\u30b9\u30ef\u30fc\u30c9\u306e\u6700\u5f8c\u306e5\u6587\u5b57\u3067\u3059\u3002", + "description": "\u30d1\u30b9\u30ef\u30fc\u30c9\u306f\u901a\u5e38\u3001\u30d0\u30c3\u30af\u30a2\u30c3\u30d7 \u30b2\u30fc\u30c8\u30a6\u30a7\u30a4\u306e\u30b7\u30ea\u30a2\u30eb\u756a\u53f7\u306e\u6700\u5f8c\u306e 5 \u6587\u5b57\u3067\u3042\u308a\u3001Tesla \u30a2\u30d7\u30ea\u3067\u78ba\u8a8d\u3059\u308b\u304b\u3001\u30d0\u30c3\u30af\u30a2\u30c3\u30d7 \u30b2\u30fc\u30c8\u30a6\u30a7\u30a4 2 \u306e\u30c9\u30a2\u306e\u5185\u5074\u306b\u3042\u308b\u30d1\u30b9\u30ef\u30fc\u30c9\u306e\u6700\u5f8c\u306e 5 \u6587\u5b57\u3092\u78ba\u8a8d\u3067\u304d\u307e\u3059\u3002", "title": "powerwall\u306e\u518d\u8a8d\u8a3c" }, "user": { @@ -29,7 +29,7 @@ "ip_address": "IP\u30a2\u30c9\u30ec\u30b9", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" }, - "description": "\u30d1\u30b9\u30ef\u30fc\u30c9\u306f\u901a\u5e38\u3001Backup Gateway\u306e\u30b7\u30ea\u30a2\u30eb\u756a\u53f7\u306e\u6700\u5f8c\u306e5\u6587\u5b57\u3067\u3042\u308a\u3001Tesla\u30a2\u30d7\u30ea\u3067\u898b\u3064\u3051\u308b\u3053\u3068\u304c\u3067\u304d\u307e\u3059\u3002\u307e\u305f\u306f\u3001Backup Gateway2\u306e\u30c9\u30a2\u306e\u5185\u5074\u306b\u3042\u308b\u30d1\u30b9\u30ef\u30fc\u30c9\u306e\u6700\u5f8c\u306e5\u6587\u5b57\u3067\u3059\u3002", + "description": "\u30d1\u30b9\u30ef\u30fc\u30c9\u306f\u901a\u5e38\u3001\u30d0\u30c3\u30af\u30a2\u30c3\u30d7 \u30b2\u30fc\u30c8\u30a6\u30a7\u30a4\u306e\u30b7\u30ea\u30a2\u30eb\u756a\u53f7\u306e\u6700\u5f8c\u306e 5 \u6587\u5b57\u3067\u3042\u308a\u3001Tesla \u30a2\u30d7\u30ea\u3067\u78ba\u8a8d\u3059\u308b\u304b\u3001\u30d0\u30c3\u30af\u30a2\u30c3\u30d7 \u30b2\u30fc\u30c8\u30a6\u30a7\u30a4 2 \u306e\u30c9\u30a2\u306e\u5185\u5074\u306b\u3042\u308b\u30d1\u30b9\u30ef\u30fc\u30c9\u306e\u6700\u5f8c\u306e 5 \u6587\u5b57\u3092\u78ba\u8a8d\u3067\u304d\u307e\u3059\u3002", "title": "Powerwall\u306b\u63a5\u7d9a" } } diff --git a/homeassistant/components/progettihwsw/switch.py b/homeassistant/components/progettihwsw/switch.py index 529e73f13dd..0aab943e385 100644 --- a/homeassistant/components/progettihwsw/switch.py +++ b/homeassistant/components/progettihwsw/switch.py @@ -1,6 +1,7 @@ """Control switches.""" from datetime import timedelta import logging +from typing import Any from ProgettiHWSW.relay import Relay import async_timeout @@ -65,17 +66,17 @@ class ProgettihwswSwitch(CoordinatorEntity, SwitchEntity): self._switch = switch self._name = name - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" await self._switch.control(True) await self.coordinator.async_request_refresh() - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the switch off.""" await self._switch.control(False) await self.coordinator.async_request_refresh() - async def async_toggle(self, **kwargs): + async def async_toggle(self, **kwargs: Any) -> None: """Toggle the state of switch.""" await self._switch.toggle() await self.coordinator.async_request_refresh() diff --git a/homeassistant/components/proliphix/climate.py b/homeassistant/components/proliphix/climate.py index 1952a6f186a..4ff1ff0906c 100644 --- a/homeassistant/components/proliphix/climate.py +++ b/homeassistant/components/proliphix/climate.py @@ -1,11 +1,14 @@ """Support for Proliphix NT10e Thermostats.""" from __future__ import annotations +from typing import Any + import proliphix import voluptuous as vol -from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( + PLATFORM_SCHEMA, + ClimateEntity, ClimateEntityFeature, HVACAction, HVACMode, @@ -63,7 +66,7 @@ class ProliphixThermostat(ClimateEntity): self._pdp = pdp self._name = None - def update(self): + def update(self) -> None: """Update the data from the thermostat.""" self._pdp.update() self._name = self._pdp.name @@ -114,7 +117,7 @@ class ProliphixThermostat(ClimateEntity): """Return available HVAC modes.""" return [] - def set_temperature(self, **kwargs): + def set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None: return diff --git a/homeassistant/components/prometheus/__init__.py b/homeassistant/components/prometheus/__init__.py index 949895cb76a..c1573755e11 100644 --- a/homeassistant/components/prometheus/__init__.py +++ b/homeassistant/components/prometheus/__init__.py @@ -8,7 +8,7 @@ import prometheus_client import voluptuous as vol from homeassistant import core as hacore -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( ATTR_CURRENT_TEMPERATURE, ATTR_HVAC_ACTION, ATTR_HVAC_MODES, @@ -17,10 +17,7 @@ from homeassistant.components.climate.const import ( HVACAction, ) from homeassistant.components.http import HomeAssistantView -from homeassistant.components.humidifier.const import ( - ATTR_AVAILABLE_MODES, - ATTR_HUMIDITY, -) +from homeassistant.components.humidifier import ATTR_AVAILABLE_MODES, ATTR_HUMIDITY from homeassistant.const import ( ATTR_BATTERY_LEVEL, ATTR_DEVICE_CLASS, @@ -43,7 +40,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_registry import EVENT_ENTITY_REGISTRY_UPDATED from homeassistant.helpers.entity_values import EntityValues from homeassistant.helpers.typing import ConfigType -from homeassistant.util.temperature import fahrenheit_to_celsius +from homeassistant.util.unit_conversion import TemperatureConverter _LOGGER = logging.getLogger(__name__) @@ -351,7 +348,9 @@ class PrometheusMetrics: with suppress(ValueError): value = self.state_as_number(state) if state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_FAHRENHEIT: - value = fahrenheit_to_celsius(value) + value = TemperatureConverter.convert( + value, TEMP_FAHRENHEIT, TEMP_CELSIUS + ) metric.labels(**self._labels(state)).set(value) def _handle_device_tracker(self, state): @@ -397,7 +396,7 @@ class PrometheusMetrics: def _handle_climate_temp(self, state, attr, metric_name, metric_description): if temp := state.attributes.get(attr): if self._climate_units == TEMP_FAHRENHEIT: - temp = fahrenheit_to_celsius(temp) + temp = TemperatureConverter.convert(temp, TEMP_FAHRENHEIT, TEMP_CELSIUS) metric = self._metric( metric_name, self.prometheus_cli.Gauge, @@ -510,7 +509,9 @@ class PrometheusMetrics: try: value = self.state_as_number(state) if state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_FAHRENHEIT: - value = fahrenheit_to_celsius(value) + value = TemperatureConverter.convert( + value, TEMP_FAHRENHEIT, TEMP_CELSIUS + ) _metric.labels(**self._labels(state)).set(value) except ValueError: pass diff --git a/homeassistant/components/prosegur/translations/es.json b/homeassistant/components/prosegur/translations/es.json index 4447bbbc2e4..80e3b80d97e 100644 --- a/homeassistant/components/prosegur/translations/es.json +++ b/homeassistant/components/prosegur/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "error": { "cannot_connect": "No se pudo conectar", diff --git a/homeassistant/components/proximity/__init__.py b/homeassistant/components/proximity/__init__.py index 8eec3e2f038..cf99f4fa503 100644 --- a/homeassistant/components/proximity/__init__.py +++ b/homeassistant/components/proximity/__init__.py @@ -1,4 +1,6 @@ """Support for tracking the proximity of a device.""" +from __future__ import annotations + import logging import voluptuous as vol @@ -15,15 +17,13 @@ from homeassistant.const import ( LENGTH_MILES, LENGTH_YARD, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, State import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import track_state_change from homeassistant.helpers.typing import ConfigType -from homeassistant.util.distance import convert from homeassistant.util.location import distance - -# mypy: allow-untyped-defs, no-check-untyped-defs +from homeassistant.util.unit_conversion import DistanceConverter _LOGGER = logging.getLogger(__name__) @@ -79,7 +79,7 @@ def setup_proximity_component( ) zone_id = f"zone.{config[CONF_ZONE]}" - proximity = Proximity( # type:ignore[no-untyped-call] + proximity = Proximity( hass, proximity_zone, DEFAULT_DIST_TO_ZONE, @@ -113,21 +113,21 @@ class Proximity(Entity): def __init__( self, - hass, - zone_friendly_name, - dist_to, - dir_of_travel, - nearest, - ignored_zones, - proximity_devices, - tolerance, - proximity_zone, - unit_of_measurement, - ): + hass: HomeAssistant, + zone_friendly_name: str, + dist_to: str, + dir_of_travel: str, + nearest: str, + ignored_zones: list[str], + proximity_devices: list[str], + tolerance: int, + proximity_zone: str, + unit_of_measurement: str, + ) -> None: """Initialize the proximity.""" self.hass = hass self.friendly_name = zone_friendly_name - self.dist_to = dist_to + self.dist_to: str | int = dist_to self.dir_of_travel = dir_of_travel self.nearest = nearest self.ignored_zones = ignored_zones @@ -137,34 +137,43 @@ class Proximity(Entity): self._unit_of_measurement = unit_of_measurement @property - def name(self): + def name(self) -> str: """Return the name of the entity.""" return self.friendly_name @property - def state(self): + def state(self) -> str | int: """Return the state.""" return self.dist_to @property - def unit_of_measurement(self): + def unit_of_measurement(self) -> str: """Return the unit of measurement of this entity.""" return self._unit_of_measurement @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, str]: """Return the state attributes.""" return {ATTR_DIR_OF_TRAVEL: self.dir_of_travel, ATTR_NEAREST: self.nearest} - def check_proximity_state_change(self, entity, old_state, new_state): + def check_proximity_state_change( + self, entity: str, old_state: State | None, new_state: State | None + ) -> None: """Perform the proximity checking.""" + if new_state is None: + return + entity_name = new_state.name devices_to_calculate = False devices_in_zone = "" zone_state = self.hass.states.get(self.proximity_zone) - proximity_latitude = zone_state.attributes.get(ATTR_LATITUDE) - proximity_longitude = zone_state.attributes.get(ATTR_LONGITUDE) + proximity_latitude = ( + zone_state.attributes.get(ATTR_LATITUDE) if zone_state else None + ) + proximity_longitude = ( + zone_state.attributes.get(ATTR_LONGITUDE) if zone_state else None + ) # Check for devices in the monitored zone. for device in self.proximity_devices: @@ -203,11 +212,11 @@ class Proximity(Entity): return # Collect distances to the zone for all devices. - distances_to_zone = {} + distances_to_zone: dict[str, float] = {} for device in self.proximity_devices: # Ignore devices in an ignored zone. device_state = self.hass.states.get(device) - if device_state.state in self.ignored_zones: + if not device_state or device_state.state in self.ignored_zones: continue # Ignore devices if proximity cannot be calculated. @@ -215,7 +224,7 @@ class Proximity(Entity): continue # Calculate the distance to the proximity zone. - dist_to_zone = distance( + proximity = distance( proximity_latitude, proximity_longitude, device_state.attributes[ATTR_LATITUDE], @@ -223,14 +232,19 @@ class Proximity(Entity): ) # Add the device and distance to a dictionary. + if not proximity: + continue distances_to_zone[device] = round( - convert(dist_to_zone, LENGTH_METERS, self.unit_of_measurement), 1 + DistanceConverter.convert( + proximity, LENGTH_METERS, self.unit_of_measurement + ), + 1, ) # Loop through each of the distances collected and work out the # closest. - closest_device: str = None - dist_to_zone: float = None + closest_device: str | None = None + dist_to_zone: float | None = None for device, zone in distances_to_zone.items(): if not dist_to_zone or zone < dist_to_zone: @@ -238,10 +252,11 @@ class Proximity(Entity): dist_to_zone = zone # If the closest device is one of the other devices. - if closest_device != entity: + if closest_device is not None and closest_device != entity: self.dist_to = round(distances_to_zone[closest_device]) self.dir_of_travel = "unknown" device_state = self.hass.states.get(closest_device) + assert device_state self.nearest = device_state.name self.schedule_update_ha_state() return @@ -256,7 +271,7 @@ class Proximity(Entity): return # Reset the variables - distance_travelled = 0 + distance_travelled: float = 0 # Calculate the distance travelled. old_distance = distance( @@ -271,6 +286,7 @@ class Proximity(Entity): new_state.attributes[ATTR_LATITUDE], new_state.attributes[ATTR_LONGITUDE], ) + assert new_distance is not None and old_distance is not None distance_travelled = round(new_distance - old_distance, 1) # Check for tolerance @@ -282,14 +298,16 @@ class Proximity(Entity): direction_of_travel = "stationary" # Update the proximity entity - self.dist_to = round(dist_to_zone) + self.dist_to = ( + round(dist_to_zone) if dist_to_zone is not None else DEFAULT_DIST_TO_ZONE + ) self.dir_of_travel = direction_of_travel self.nearest = entity_name self.schedule_update_ha_state() _LOGGER.debug( "proximity.%s update entity: distance=%s: direction=%s: device=%s", self.friendly_name, - round(dist_to_zone), + self.dist_to, direction_of_travel, entity_name, ) diff --git a/homeassistant/components/proxmoxve/binary_sensor.py b/homeassistant/components/proxmoxve/binary_sensor.py index 97995431778..fdc10eda2dd 100644 --- a/homeassistant/components/proxmoxve/binary_sensor.py +++ b/homeassistant/components/proxmoxve/binary_sensor.py @@ -93,7 +93,7 @@ class ProxmoxBinarySensor(ProxmoxEntity, BinarySensorEntity): return data["status"] == "running" @property - def available(self): + def available(self) -> bool: """Return sensor availability.""" return super().available and self.coordinator.data is not None diff --git a/homeassistant/components/prusalink/__init__.py b/homeassistant/components/prusalink/__init__.py index cbc77b92e8a..2b77a6fc524 100644 --- a/homeassistant/components/prusalink/__init__.py +++ b/homeassistant/components/prusalink/__init__.py @@ -4,6 +4,7 @@ from __future__ import annotations from abc import abstractmethod from datetime import timedelta import logging +from time import monotonic from typing import Generic, TypeVar import async_timeout @@ -11,7 +12,7 @@ from pyprusalink import InvalidAuth, JobInfo, PrinterInfo, PrusaLink, PrusaLinkE from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import ( @@ -22,7 +23,7 @@ from homeassistant.helpers.update_coordinator import ( from .const import DOMAIN -PLATFORMS: list[Platform] = [Platform.SENSOR, Platform.CAMERA] +PLATFORMS: list[Platform] = [Platform.BUTTON, Platform.CAMERA, Platform.SENSOR] _LOGGER = logging.getLogger(__name__) @@ -63,30 +64,46 @@ class PrusaLinkUpdateCoordinator(DataUpdateCoordinator, Generic[T]): """Update coordinator for the printer.""" config_entry: ConfigEntry + expect_change_until = 0.0 def __init__(self, hass: HomeAssistant, api: PrusaLink) -> None: """Initialize the update coordinator.""" self.api = api super().__init__( - hass, _LOGGER, name=DOMAIN, update_interval=timedelta(seconds=30) + hass, _LOGGER, name=DOMAIN, update_interval=self._get_update_interval(None) ) async def _async_update_data(self) -> T: """Update the data.""" try: with async_timeout.timeout(5): - return await self._fetch_data() + data = await self._fetch_data() except InvalidAuth: raise UpdateFailed("Invalid authentication") from None except PrusaLinkError as err: raise UpdateFailed(str(err)) from err + self.update_interval = self._get_update_interval(data) + return data + @abstractmethod async def _fetch_data(self) -> T: """Fetch the actual data.""" raise NotImplementedError + @callback + def expect_change(self) -> None: + """Expect a change.""" + self.expect_change_until = monotonic() + 30 + + def _get_update_interval(self, data: T) -> timedelta: + """Get new update interval.""" + if self.expect_change_until > monotonic(): + return timedelta(seconds=5) + + return timedelta(seconds=30) + class PrinterUpdateCoordinator(PrusaLinkUpdateCoordinator[PrinterInfo]): """Printer update coordinator.""" @@ -95,6 +112,15 @@ class PrinterUpdateCoordinator(PrusaLinkUpdateCoordinator[PrinterInfo]): """Fetch the printer data.""" return await self.api.get_printer() + def _get_update_interval(self, data: T) -> timedelta: + """Get new update interval.""" + if data and any( + data["state"]["flags"][key] for key in ("pausing", "cancelling") + ): + return timedelta(seconds=5) + + return super()._get_update_interval(data) + class JobUpdateCoordinator(PrusaLinkUpdateCoordinator[JobInfo]): """Job update coordinator.""" diff --git a/homeassistant/components/prusalink/button.py b/homeassistant/components/prusalink/button.py new file mode 100644 index 00000000000..7c234616311 --- /dev/null +++ b/homeassistant/components/prusalink/button.py @@ -0,0 +1,126 @@ +"""PrusaLink sensors.""" +from __future__ import annotations + +from collections.abc import Callable, Coroutine +from dataclasses import dataclass +from typing import Any, Generic, TypeVar, cast + +from pyprusalink import Conflict, JobInfo, PrinterInfo, PrusaLink + +from homeassistant.components.button import ButtonEntity, ButtonEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import DOMAIN, PrusaLinkEntity, PrusaLinkUpdateCoordinator + +T = TypeVar("T", PrinterInfo, JobInfo) + + +@dataclass +class PrusaLinkButtonEntityDescriptionMixin(Generic[T]): + """Mixin for required keys.""" + + press_fn: Callable[[PrusaLink], Coroutine[Any, Any, None]] + + +@dataclass +class PrusaLinkButtonEntityDescription( + ButtonEntityDescription, PrusaLinkButtonEntityDescriptionMixin[T], Generic[T] +): + """Describes PrusaLink button entity.""" + + available_fn: Callable[[T], bool] = lambda _: True + + +BUTTONS: dict[str, tuple[PrusaLinkButtonEntityDescription, ...]] = { + "printer": ( + PrusaLinkButtonEntityDescription[PrinterInfo]( + key="printer.cancel_job", + name="Cancel Job", + press_fn=lambda api: cast(Coroutine, api.cancel_job()), + available_fn=lambda data: any( + data["state"]["flags"][flag] + for flag in ("printing", "pausing", "paused") + ), + ), + PrusaLinkButtonEntityDescription[PrinterInfo]( + key="job.pause_job", + name="Pause Job", + press_fn=lambda api: cast(Coroutine, api.pause_job()), + available_fn=lambda data: ( + data["state"]["flags"]["printing"] + and not data["state"]["flags"]["paused"] + ), + ), + PrusaLinkButtonEntityDescription[PrinterInfo]( + key="job.resume_job", + name="Resume Job", + press_fn=lambda api: cast(Coroutine, api.resume_job()), + available_fn=lambda data: cast(bool, data["state"]["flags"]["paused"]), + ), + ), +} + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up PrusaLink buttons based on a config entry.""" + coordinators: dict[str, PrusaLinkUpdateCoordinator] = hass.data[DOMAIN][ + entry.entry_id + ] + + entities: list[PrusaLinkEntity] = [] + + for coordinator_type, sensors in BUTTONS.items(): + coordinator = coordinators[coordinator_type] + entities.extend( + PrusaLinkButtonEntity(coordinator, sensor_description) + for sensor_description in sensors + ) + + async_add_entities(entities) + + +class PrusaLinkButtonEntity(PrusaLinkEntity, ButtonEntity): + """Defines a PrusaLink button.""" + + entity_description: PrusaLinkButtonEntityDescription + + def __init__( + self, + coordinator: PrusaLinkUpdateCoordinator, + description: PrusaLinkButtonEntityDescription, + ) -> None: + """Initialize a PrusaLink sensor entity.""" + super().__init__(coordinator=coordinator) + self.entity_description = description + self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{description.key}" + + @property + def available(self) -> bool: + """Return if sensor is available.""" + return super().available and self.entity_description.available_fn( + self.coordinator.data + ) + + async def async_press(self) -> None: + """Press the button.""" + try: + await self.entity_description.press_fn(self.coordinator.api) + except Conflict as err: + raise HomeAssistantError( + "Action conflicts with current printer state" + ) from err + + coordinators: dict[str, PrusaLinkUpdateCoordinator] = self.hass.data[DOMAIN][ + self.coordinator.config_entry.entry_id + ] + + for coordinator in coordinators.values(): + coordinator.expect_change() + await coordinator.async_request_refresh() diff --git a/homeassistant/components/prusalink/manifest.json b/homeassistant/components/prusalink/manifest.json index 9efed0be74a..f662620b90a 100644 --- a/homeassistant/components/prusalink/manifest.json +++ b/homeassistant/components/prusalink/manifest.json @@ -3,7 +3,7 @@ "name": "PrusaLink", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/prusalink", - "requirements": ["pyprusalink==1.0.1"], + "requirements": ["pyprusalink==1.1.0"], "dhcp": [ { "macaddress": "109C70*" diff --git a/homeassistant/components/prusalink/translations/bg.json b/homeassistant/components/prusalink/translations/bg.json new file mode 100644 index 00000000000..f0eea7f98b2 --- /dev/null +++ b/homeassistant/components/prusalink/translations/bg.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "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", + "not_supported": "\u041f\u043e\u0434\u0434\u044a\u0440\u0436\u0430 \u0441\u0435 \u0441\u0430\u043c\u043e PrusaLink API v2", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "user": { + "data": { + "api_key": "API \u043a\u043b\u044e\u0447", + "host": "\u0425\u043e\u0441\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prusalink/translations/cs.json b/homeassistant/components/prusalink/translations/cs.json new file mode 100644 index 00000000000..323cf668090 --- /dev/null +++ b/homeassistant/components/prusalink/translations/cs.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "api_key": "Kl\u00ed\u010d API", + "host": "Hostitel" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prusalink/translations/el.json b/homeassistant/components/prusalink/translations/el.json new file mode 100644 index 00000000000..8116be1ad89 --- /dev/null +++ b/homeassistant/components/prusalink/translations/el.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "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", + "not_supported": "\u03a5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03bc\u03cc\u03bd\u03bf \u03c4\u03bf PrusaLink API v2", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, + "step": { + "user": { + "data": { + "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API", + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prusalink/translations/fr.json b/homeassistant/components/prusalink/translations/fr.json new file mode 100644 index 00000000000..372360dbde8 --- /dev/null +++ b/homeassistant/components/prusalink/translations/fr.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "\u00c9chec de connexion", + "invalid_auth": "Authentification non valide", + "not_supported": "Seule l'API PrusaLink v2 est prise en charge", + "unknown": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "api_key": "Cl\u00e9 d'API", + "host": "H\u00f4te" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prusalink/translations/hu.json b/homeassistant/components/prusalink/translations/hu.json new file mode 100644 index 00000000000..7fe5692714a --- /dev/null +++ b/homeassistant/components/prusalink/translations/hu.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "not_supported": "Csak a PrusaLink API v2 t\u00e1mogatott", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "step": { + "user": { + "data": { + "api_key": "API kulcs", + "host": "C\u00edm" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prusalink/translations/id.json b/homeassistant/components/prusalink/translations/id.json new file mode 100644 index 00000000000..3582565c801 --- /dev/null +++ b/homeassistant/components/prusalink/translations/id.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid", + "not_supported": "Hanya API PrusaLink v2 yang didukung", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "user": { + "data": { + "api_key": "Kunci API", + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prusalink/translations/it.json b/homeassistant/components/prusalink/translations/it.json new file mode 100644 index 00000000000..02b2b8a53aa --- /dev/null +++ b/homeassistant/components/prusalink/translations/it.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida", + "not_supported": "\u00c8 supportata solo l'API PrusaLink v2", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "api_key": "Chiave API", + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prusalink/translations/ja.json b/homeassistant/components/prusalink/translations/ja.json new file mode 100644 index 00000000000..95a57633cbd --- /dev/null +++ b/homeassistant/components/prusalink/translations/ja.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "not_supported": "PrusaLink API v2\u306e\u307f\u5bfe\u5fdc", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, + "step": { + "user": { + "data": { + "api_key": "API\u30ad\u30fc", + "host": "\u30db\u30b9\u30c8" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prusalink/translations/nl.json b/homeassistant/components/prusalink/translations/nl.json new file mode 100644 index 00000000000..2a250129c18 --- /dev/null +++ b/homeassistant/components/prusalink/translations/nl.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "Kan geen verbinding maken", + "invalid_auth": "Ongeldige authenticatie", + "not_supported": "Alleen PrusaLink API V2 wordt ondersteund", + "unknown": "Onverwachte fout" + }, + "step": { + "user": { + "data": { + "api_key": "API-sleutel", + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prusalink/translations/pt.json b/homeassistant/components/prusalink/translations/pt.json new file mode 100644 index 00000000000..5003d44e3d9 --- /dev/null +++ b/homeassistant/components/prusalink/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "not_supported": "Apenas a API PrusaLink v2 \u00e9 suportada" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prusalink/translations/sensor.bg.json b/homeassistant/components/prusalink/translations/sensor.bg.json new file mode 100644 index 00000000000..e44192730e7 --- /dev/null +++ b/homeassistant/components/prusalink/translations/sensor.bg.json @@ -0,0 +1,7 @@ +{ + "state": { + "prusalink__printer_state": { + "printing": "\u041e\u0442\u043f\u0435\u0447\u0430\u0442\u0432\u0430\u043d\u0435" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prusalink/translations/sensor.cs.json b/homeassistant/components/prusalink/translations/sensor.cs.json new file mode 100644 index 00000000000..53663d41858 --- /dev/null +++ b/homeassistant/components/prusalink/translations/sensor.cs.json @@ -0,0 +1,9 @@ +{ + "state": { + "prusalink__printer_state": { + "cancelling": "Prob\u00edh\u00e1 zru\u0161en\u00ed", + "idle": "Ne\u010dinn\u00fd", + "paused": "Pozastaveno" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prusalink/translations/sensor.el.json b/homeassistant/components/prusalink/translations/sensor.el.json new file mode 100644 index 00000000000..f391a4d238e --- /dev/null +++ b/homeassistant/components/prusalink/translations/sensor.el.json @@ -0,0 +1,11 @@ +{ + "state": { + "prusalink__printer_state": { + "cancelling": "\u0391\u03ba\u03cd\u03c1\u03c9\u03c3\u03b7", + "idle": "\u0391\u03b4\u03c1\u03b1\u03bd\u03ae\u03c2", + "paused": "\u03a3\u03b5 \u03c0\u03b1\u03cd\u03c3\u03b7", + "pausing": "\u03a0\u03b1\u03cd\u03c3\u03b7", + "printing": "\u0395\u03ba\u03c4\u03cd\u03c0\u03c9\u03c3\u03b7" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prusalink/translations/sensor.fr.json b/homeassistant/components/prusalink/translations/sensor.fr.json new file mode 100644 index 00000000000..e1134b4f55c --- /dev/null +++ b/homeassistant/components/prusalink/translations/sensor.fr.json @@ -0,0 +1,11 @@ +{ + "state": { + "prusalink__printer_state": { + "cancelling": "Annulation", + "idle": "Inactif", + "paused": "En pause", + "pausing": "Mise en pause", + "printing": "Impression" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prusalink/translations/sensor.hu.json b/homeassistant/components/prusalink/translations/sensor.hu.json new file mode 100644 index 00000000000..ab836fe34f8 --- /dev/null +++ b/homeassistant/components/prusalink/translations/sensor.hu.json @@ -0,0 +1,11 @@ +{ + "state": { + "prusalink__printer_state": { + "cancelling": "\u00c9rv\u00e9nytelen\u00edt\u00e9sben", + "idle": "T\u00e9tlen", + "paused": "Sz\u00fcneteltetve", + "pausing": "Sz\u00fcneteltet\u00e9sben", + "printing": "Nyomtat\u00e1sban" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prusalink/translations/sensor.id.json b/homeassistant/components/prusalink/translations/sensor.id.json new file mode 100644 index 00000000000..d09930825fe --- /dev/null +++ b/homeassistant/components/prusalink/translations/sensor.id.json @@ -0,0 +1,11 @@ +{ + "state": { + "prusalink__printer_state": { + "cancelling": "Membatalkan", + "idle": "Siaga", + "paused": "Dijeda", + "pausing": "Jeda", + "printing": "Mencetak" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prusalink/translations/sensor.it.json b/homeassistant/components/prusalink/translations/sensor.it.json new file mode 100644 index 00000000000..7336cd6c2bf --- /dev/null +++ b/homeassistant/components/prusalink/translations/sensor.it.json @@ -0,0 +1,11 @@ +{ + "state": { + "prusalink__printer_state": { + "cancelling": "In annullamento", + "idle": "Inattiva", + "paused": "Fermata", + "pausing": "In pausa", + "printing": "In stampa" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prusalink/translations/sensor.ja.json b/homeassistant/components/prusalink/translations/sensor.ja.json new file mode 100644 index 00000000000..43a54856edb --- /dev/null +++ b/homeassistant/components/prusalink/translations/sensor.ja.json @@ -0,0 +1,11 @@ +{ + "state": { + "prusalink__printer_state": { + "cancelling": "\u30ad\u30e3\u30f3\u30bb\u30eb\u4e2d", + "idle": "\u30a2\u30a4\u30c9\u30eb", + "paused": "\u4e00\u6642\u505c\u6b62", + "pausing": "\u4e00\u6642\u505c\u6b62\u4e2d", + "printing": "\u5370\u5237" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prusalink/translations/sensor.nl.json b/homeassistant/components/prusalink/translations/sensor.nl.json new file mode 100644 index 00000000000..0dfc3902f68 --- /dev/null +++ b/homeassistant/components/prusalink/translations/sensor.nl.json @@ -0,0 +1,10 @@ +{ + "state": { + "prusalink__printer_state": { + "cancelling": "Annuleren", + "idle": "Inactief", + "paused": "Gepauzeerd", + "printing": "Afdrukken" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prusalink/translations/sensor.pl.json b/homeassistant/components/prusalink/translations/sensor.pl.json index a28232a63a1..10f21587c74 100644 --- a/homeassistant/components/prusalink/translations/sensor.pl.json +++ b/homeassistant/components/prusalink/translations/sensor.pl.json @@ -1,11 +1,11 @@ { "state": { "prusalink__printer_state": { - "cancelling": "Anulowanie", - "idle": "Bezczynny", - "paused": "Wstrzymany", - "pausing": "Wstrzymywanie", - "printing": "Drukowanie" + "cancelling": "anulowanie", + "idle": "bezczynny", + "paused": "wstrzymany", + "pausing": "wstrzymywanie", + "printing": "drukowanie" } } } \ No newline at end of file diff --git a/homeassistant/components/prusalink/translations/sensor.pt.json b/homeassistant/components/prusalink/translations/sensor.pt.json new file mode 100644 index 00000000000..462b35d9722 --- /dev/null +++ b/homeassistant/components/prusalink/translations/sensor.pt.json @@ -0,0 +1,11 @@ +{ + "state": { + "prusalink__printer_state": { + "cancelling": "Cancelando", + "idle": "Ocioso", + "paused": "Pausado", + "pausing": "Pausando", + "printing": "Impress\u00e3o" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prusalink/translations/sensor.sv.json b/homeassistant/components/prusalink/translations/sensor.sv.json new file mode 100644 index 00000000000..47f894b1dcd --- /dev/null +++ b/homeassistant/components/prusalink/translations/sensor.sv.json @@ -0,0 +1,11 @@ +{ + "state": { + "prusalink__printer_state": { + "cancelling": "Avbryter", + "idle": "Inaktiv", + "paused": "Pausad", + "pausing": "Pausar", + "printing": "Skriver ut" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prusalink/translations/sensor.tr.json b/homeassistant/components/prusalink/translations/sensor.tr.json new file mode 100644 index 00000000000..32dab7904bc --- /dev/null +++ b/homeassistant/components/prusalink/translations/sensor.tr.json @@ -0,0 +1,11 @@ +{ + "state": { + "prusalink__printer_state": { + "cancelling": "\u0130ptal", + "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/sv.json b/homeassistant/components/prusalink/translations/sv.json new file mode 100644 index 00000000000..56d0b970314 --- /dev/null +++ b/homeassistant/components/prusalink/translations/sv.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "not_supported": "Endast PrusaLink API v2 st\u00f6ds", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "data": { + "api_key": "API-nyckel", + "host": "V\u00e4rd" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prusalink/translations/tr.json b/homeassistant/components/prusalink/translations/tr.json new file mode 100644 index 00000000000..4658856d2fa --- /dev/null +++ b/homeassistant/components/prusalink/translations/tr.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "not_supported": "Yaln\u0131zca PrusaLink API v2 desteklenir", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "api_key": "API Anahtar\u0131", + "host": "Sunucu" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ps4/__init__.py b/homeassistant/components/ps4/__init__.py index e480396e6a2..9a5744de2fd 100644 --- a/homeassistant/components/ps4/__init__.py +++ b/homeassistant/components/ps4/__init__.py @@ -7,10 +7,10 @@ from pyps4_2ndscreen.media_art import COUNTRIES import voluptuous as vol from homeassistant.components import persistent_notification -from homeassistant.components.media_player.const import ( +from homeassistant.components.media_player import ( ATTR_MEDIA_CONTENT_TYPE, ATTR_MEDIA_TITLE, - MEDIA_TYPE_GAME, + MediaType, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -206,7 +206,7 @@ def _reformat_data(hass: HomeAssistant, games: dict, unique_id: str) -> dict: ATTR_LOCKED: False, ATTR_MEDIA_TITLE: data, ATTR_MEDIA_IMAGE_URL: None, - ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_GAME, + ATTR_MEDIA_CONTENT_TYPE: MediaType.GAME, } data_reformatted = True diff --git a/homeassistant/components/ps4/media_player.py b/homeassistant/components/ps4/media_player.py index 1a1c93e210a..5202825c85f 100644 --- a/homeassistant/components/ps4/media_player.py +++ b/homeassistant/components/ps4/media_player.py @@ -8,14 +8,12 @@ from pyps4_2ndscreen.media_art import TYPE_APP as PS_TYPE_APP import pyps4_2ndscreen.ps4 as pyps4 from homeassistant.components.media_player import ( - MediaPlayerEntity, - MediaPlayerEntityFeature, -) -from homeassistant.components.media_player.const import ( ATTR_MEDIA_CONTENT_TYPE, ATTR_MEDIA_TITLE, - MEDIA_TYPE_APP, - MEDIA_TYPE_GAME, + MediaPlayerEntity, + MediaPlayerEntityFeature, + MediaPlayerState, + MediaType, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -24,9 +22,6 @@ from homeassistant.const import ( CONF_NAME, CONF_REGION, CONF_TOKEN, - STATE_IDLE, - STATE_PLAYING, - STATE_STANDBY, ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry, entity_registry @@ -130,12 +125,12 @@ class PS4Device(MediaPlayerEntity): self._region, ) - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Subscribe PS4 events.""" self.hass.data[PS4_DATA].devices.append(self) self.check_region() - async def async_update(self): + async def async_update(self) -> None: """Retrieve the latest data.""" if self._ps4.ddp_protocol is not None: # Request Status with asyncio transport. @@ -177,7 +172,7 @@ class PS4Device(MediaPlayerEntity): name = status.get("running-app-name") if title_id and name is not None: - self._state = STATE_PLAYING + self._state = MediaPlayerState.PLAYING if self._media_content_id != title_id: self._media_content_id = title_id @@ -191,10 +186,10 @@ class PS4Device(MediaPlayerEntity): # Get data from PS Store. asyncio.ensure_future(self.async_get_title_data(title_id, name)) else: - if self._state != STATE_IDLE: + if self._state != MediaPlayerState.IDLE: self.idle() else: - if self._state != STATE_STANDBY: + if self._state != MediaPlayerState.STANDBY: self.state_standby() elif self._retry > DEFAULT_RETRIES: @@ -219,12 +214,12 @@ class PS4Device(MediaPlayerEntity): def idle(self): """Set states for state idle.""" self.reset_title() - self._state = STATE_IDLE + self._state = MediaPlayerState.IDLE def state_standby(self): """Set states for state standby.""" self.reset_title() - self._state = STATE_STANDBY + self._state = MediaPlayerState.STANDBY def state_unknown(self): """Set states for state unknown.""" @@ -265,9 +260,9 @@ class PS4Device(MediaPlayerEntity): art = title.cover_art # Assume media type is game if not app. if title.game_type != PS_TYPE_APP: - media_type = MEDIA_TYPE_GAME + media_type = MediaType.GAME else: - media_type = MEDIA_TYPE_APP + media_type = MediaType.APP else: _LOGGER.error( "Could not find data in region: %s for PS ID: %s", @@ -365,7 +360,7 @@ class PS4Device(MediaPlayerEntity): self._unique_id = format_unique_id(self._creds, status["host-id"]) - async def async_will_remove_from_hass(self): + async def async_will_remove_from_hass(self) -> None: """Remove Entity from Home Assistant.""" # Close TCP Transport. if self._ps4.connected: @@ -382,7 +377,7 @@ class PS4Device(MediaPlayerEntity): def entity_picture(self): """Return picture.""" if ( - self._state == STATE_PLAYING + self._state == MediaPlayerState.PLAYING and self._media_content_id is not None and (image_hash := self.media_image_hash) is not None ): @@ -439,27 +434,27 @@ class PS4Device(MediaPlayerEntity): """List of available input sources.""" return self._source_list - async def async_turn_off(self): + async def async_turn_off(self) -> None: """Turn off media player.""" await self._ps4.standby() - async def async_turn_on(self): + async def async_turn_on(self) -> None: """Turn on the media player.""" self._ps4.wakeup() - async def async_toggle(self): + async def async_toggle(self) -> None: """Toggle media player.""" await self._ps4.toggle() - async def async_media_pause(self): + async def async_media_pause(self) -> None: """Send keypress ps to return to menu.""" await self.async_send_remote_control("ps") - async def async_media_stop(self): + async def async_media_stop(self) -> None: """Send keypress ps to return to menu.""" await self.async_send_remote_control("ps") - async def async_select_source(self, source): + async def async_select_source(self, source: str) -> None: """Select input source.""" for title_id, data in self._games.items(): game = data[ATTR_MEDIA_TITLE] diff --git a/homeassistant/components/ps4/translations/fr.json b/homeassistant/components/ps4/translations/fr.json index 73e2e18e22a..cc2332af9b0 100644 --- a/homeassistant/components/ps4/translations/fr.json +++ b/homeassistant/components/ps4/translations/fr.json @@ -9,7 +9,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "credential_timeout": "Le service d'informations d'identification a expir\u00e9. Appuyez sur soumettre pour red\u00e9marrer.", + "credential_timeout": "Le service d'informations d'identification a expir\u00e9. Appuyez sur \u00ab\u00a0Valider\u00a0\u00bb pour red\u00e9marrer.", "login_failed": "\u00c9chec de l'association \u00e0 la PlayStation 4. V\u00e9rifiez que le code PIN est correct.", "no_ipaddress": "Entrez l'adresse IP de la PlayStation 4 que vous souhaitez configurer." }, diff --git a/homeassistant/components/pulseaudio_loopback/switch.py b/homeassistant/components/pulseaudio_loopback/switch.py index df28b0ff38c..92dfb235b1b 100644 --- a/homeassistant/components/pulseaudio_loopback/switch.py +++ b/homeassistant/components/pulseaudio_loopback/switch.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any from pulsectl import Pulse, PulseError import voluptuous as vol @@ -100,7 +101,7 @@ class PALoopbackSwitch(SwitchEntity): return None @property - def available(self): + def available(self) -> bool: """Return true when connected to server.""" return self._pa_svr.connected @@ -114,7 +115,7 @@ class PALoopbackSwitch(SwitchEntity): """Return true if device is on.""" return self._module_idx is not None - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" if not self.is_on: self._pa_svr.module_load( @@ -124,13 +125,13 @@ class PALoopbackSwitch(SwitchEntity): else: _LOGGER.warning(IGNORED_SWITCH_WARN) - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" if self.is_on: self._pa_svr.module_unload(self._module_idx) else: _LOGGER.warning(IGNORED_SWITCH_WARN) - def update(self): + def update(self) -> None: """Refresh state in case an alternate process modified this data.""" self._module_idx = self._get_module_idx() diff --git a/homeassistant/components/pure_energie/translations/id.json b/homeassistant/components/pure_energie/translations/id.json index ef0e2b69785..55fea559b74 100644 --- a/homeassistant/components/pure_energie/translations/id.json +++ b/homeassistant/components/pure_energie/translations/id.json @@ -18,7 +18,7 @@ } }, "zeroconf_confirm": { - "description": "Ingin menambahkan Pure Energie Meter (`{name}`) ke Home Assistant?", + "description": "Ingin menambahkan Pure Energie Meter (`{model}`) ke Home Assistant?", "title": "Peranti Pure Energie Meter yang ditemukan" } } diff --git a/homeassistant/components/pure_energie/translations/pl.json b/homeassistant/components/pure_energie/translations/pl.json index 526326fccc1..891393e1d2f 100644 --- a/homeassistant/components/pure_energie/translations/pl.json +++ b/homeassistant/components/pure_energie/translations/pl.json @@ -12,6 +12,9 @@ "user": { "data": { "host": "Nazwa hosta lub adres IP" + }, + "data_description": { + "host": "Adres IP lub nazwa hosta Pure Energie Meter." } }, "zeroconf_confirm": { diff --git a/homeassistant/components/pure_energie/translations/pt.json b/homeassistant/components/pure_energie/translations/pt.json index ce7cbc3f548..aad646d8e8c 100644 --- a/homeassistant/components/pure_energie/translations/pt.json +++ b/homeassistant/components/pure_energie/translations/pt.json @@ -4,6 +4,9 @@ "user": { "data": { "host": "Servidor" + }, + "data_description": { + "host": "O endere\u00e7o IP ou nome de host do seu Medidor Pure Energie." } } } diff --git a/homeassistant/components/pure_energie/translations/sv.json b/homeassistant/components/pure_energie/translations/sv.json index d85762bb0b4..b77f7babf41 100644 --- a/homeassistant/components/pure_energie/translations/sv.json +++ b/homeassistant/components/pure_energie/translations/sv.json @@ -12,6 +12,9 @@ "user": { "data": { "host": "V\u00e4rd" + }, + "data_description": { + "host": "IP-adressen eller v\u00e4rdnamnet f\u00f6r din Pure Energie-m\u00e4tare." } }, "zeroconf_confirm": { diff --git a/homeassistant/components/pure_energie/translations/tr.json b/homeassistant/components/pure_energie/translations/tr.json index 8c2a8402124..7af9d18c26e 100644 --- a/homeassistant/components/pure_energie/translations/tr.json +++ b/homeassistant/components/pure_energie/translations/tr.json @@ -12,6 +12,9 @@ "user": { "data": { "host": "Sunucu" + }, + "data_description": { + "host": "Pure Energie Meter cihaz\u0131n\u0131z\u0131n IP adresi veya ana bilgisayar ad\u0131." } }, "zeroconf_confirm": { diff --git a/homeassistant/components/push/camera.py b/homeassistant/components/push/camera.py index f70ebd3a3a9..77bcf63e17e 100644 --- a/homeassistant/components/push/camera.py +++ b/homeassistant/components/push/camera.py @@ -11,8 +11,7 @@ import async_timeout import voluptuous as vol from homeassistant.components import webhook -from homeassistant.components.camera import PLATFORM_SCHEMA, STATE_IDLE, Camera -from homeassistant.components.camera.const import DOMAIN +from homeassistant.components.camera import DOMAIN, PLATFORM_SCHEMA, STATE_IDLE, Camera from homeassistant.const import CONF_NAME, CONF_TIMEOUT, CONF_WEBHOOK_ID from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv @@ -109,7 +108,7 @@ class PushCamera(Camera): self.webhook_id = webhook_id self.webhook_url = webhook.async_generate_url(hass, webhook_id) - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Call when entity is added to hass.""" self.hass.data[PUSH_CAMERA_DATA][self.webhook_id] = self diff --git a/homeassistant/components/pushbullet/sensor.py b/homeassistant/components/pushbullet/sensor.py index a3d9b549e95..51a18f1aaea 100644 --- a/homeassistant/components/pushbullet/sensor.py +++ b/homeassistant/components/pushbullet/sensor.py @@ -114,7 +114,7 @@ class PushBulletNotificationSensor(SensorEntity): self._attr_name = f"Pushbullet {description.key}" - def update(self): + def update(self) -> None: """Fetch the latest data from the sensor. This will fetch the 'sensor reading' into self._state but also all diff --git a/homeassistant/components/pushover/__init__.py b/homeassistant/components/pushover/__init__.py index fa9a9c5ebd9..3c0c92db044 100644 --- a/homeassistant/components/pushover/__init__.py +++ b/homeassistant/components/pushover/__init__.py @@ -25,6 +25,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up pushover from a config entry.""" + # remove unique_id for beta users + if entry.unique_id is not None: + hass.config_entries.async_update_entry(entry, unique_id=None) + pushover_api = PushoverAPI(entry.data[CONF_API_KEY]) try: await hass.async_add_executor_job( diff --git a/homeassistant/components/pushover/config_flow.py b/homeassistant/components/pushover/config_flow.py index 3f12446733e..ddb61d4bbc3 100644 --- a/homeassistant/components/pushover/config_flow.py +++ b/homeassistant/components/pushover/config_flow.py @@ -62,6 +62,12 @@ class PushBulletConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors = {} if user_input is not None and self._reauth_entry: user_input = {**self._reauth_entry.data, **user_input} + self._async_abort_entries_match( + { + CONF_USER_KEY: user_input[CONF_USER_KEY], + CONF_API_KEY: user_input[CONF_API_KEY], + } + ) errors = await validate_input(self.hass, user_input) if not errors: self.hass.config_entries.async_update_entry( @@ -87,9 +93,13 @@ class PushBulletConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors = {} if user_input is not None: - await self.async_set_unique_id(user_input[CONF_USER_KEY]) - self._abort_if_unique_id_configured() + self._async_abort_entries_match( + { + CONF_USER_KEY: user_input[CONF_USER_KEY], + CONF_API_KEY: user_input[CONF_API_KEY], + } + ) self._async_abort_entries_match({CONF_NAME: user_input[CONF_NAME]}) errors = await validate_input(self.hass, user_input) diff --git a/homeassistant/components/pushover/translations/bg.json b/homeassistant/components/pushover/translations/bg.json new file mode 100644 index 00000000000..36e77da587c --- /dev/null +++ b/homeassistant/components/pushover/translations/bg.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u043b\u0443\u0433\u0430\u0442\u0430 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0430", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" + }, + "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", + "invalid_user_key": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u0438 \u043a\u043b\u044e\u0447" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "API \u043a\u043b\u044e\u0447" + }, + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u043d\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430" + }, + "user": { + "data": { + "api_key": "API \u043a\u043b\u044e\u0447", + "name": "\u0418\u043c\u0435", + "user_key": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u0438 \u043a\u043b\u044e\u0447" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pushover/translations/cs.json b/homeassistant/components/pushover/translations/cs.json new file mode 100644 index 00000000000..55a2608b01f --- /dev/null +++ b/homeassistant/components/pushover/translations/cs.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Slu\u017eba je ji\u017e nastavena", + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_api_key": "Neplatn\u00fd kl\u00ed\u010d API" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "Kl\u00ed\u010d API" + }, + "title": "Znovu ov\u011b\u0159it integraci" + }, + "user": { + "data": { + "api_key": "Kl\u00ed\u010d API", + "name": "Jm\u00e9no" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pushover/translations/es.json b/homeassistant/components/pushover/translations/es.json index c36644e575e..e28f7045aa0 100644 --- a/homeassistant/components/pushover/translations/es.json +++ b/homeassistant/components/pushover/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "El servicio ya est\u00e1 configurado", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "error": { "cannot_connect": "No se pudo conectar", diff --git a/homeassistant/components/pushover/translations/fr.json b/homeassistant/components/pushover/translations/fr.json index a8d7a213d64..2982b17064c 100644 --- a/homeassistant/components/pushover/translations/fr.json +++ b/homeassistant/components/pushover/translations/fr.json @@ -24,5 +24,10 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "title": "La configuration YAML pour Pushover sera bient\u00f4t supprim\u00e9e" + } } } \ No newline at end of file diff --git a/homeassistant/components/pushover/translations/pl.json b/homeassistant/components/pushover/translations/pl.json index 01f297f75cc..02195af9229 100644 --- a/homeassistant/components/pushover/translations/pl.json +++ b/homeassistant/components/pushover/translations/pl.json @@ -6,7 +6,8 @@ }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", - "invalid_api_key": "Nieprawid\u0142owy klucz API" + "invalid_api_key": "Nieprawid\u0142owy klucz API", + "invalid_user_key": "Nieprawid\u0142owy klucz u\u017cytkownika" }, "step": { "reauth_confirm": { @@ -18,9 +19,16 @@ "user": { "data": { "api_key": "Klucz API", - "name": "Nazwa" + "name": "Nazwa", + "user_key": "Klucz u\u017cytkownika" } } } + }, + "issues": { + "deprecated_yaml": { + "description": "Konfiguracja Pushover przy u\u017cyciu YAML zostanie usuni\u0119ta. \n\nTwoja istniej\u0105ca konfiguracja YAML zosta\u0142a automatycznie zaimportowana do interfejsu u\u017cytkownika. \n\nUsu\u0144 konfiguracj\u0119 YAML z pliku configuration.yaml i uruchom ponownie Home Assistanta, aby rozwi\u0105za\u0107 ten problem.", + "title": "Konfiguracja YAML dla Pushover zostanie usuni\u0119ta" + } } } \ No newline at end of file diff --git a/homeassistant/components/pushover/translations/pt.json b/homeassistant/components/pushover/translations/pt.json new file mode 100644 index 00000000000..519ac06a01f --- /dev/null +++ b/homeassistant/components/pushover/translations/pt.json @@ -0,0 +1,17 @@ +{ + "config": { + "step": { + "user": { + "data": { + "user_key": "Chave do usu\u00e1rio" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "A configura\u00e7\u00e3o do Pushover usando YAML est\u00e1 sendo removida. \n\n Sua configura\u00e7\u00e3o YAML existente foi importada para a interface do usu\u00e1rio automaticamente. \n\n Remova a configura\u00e7\u00e3o Pushover YAML do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "A configura\u00e7\u00e3o do Pushover YAML est\u00e1 sendo removida" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pushover/translations/sv.json b/homeassistant/components/pushover/translations/sv.json new file mode 100644 index 00000000000..5e85503bb4b --- /dev/null +++ b/homeassistant/components/pushover/translations/sv.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "Tj\u00e4nsten \u00e4r redan konfigurerad", + "reauth_successful": "\u00c5terautentisering lyckades" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_api_key": "Ogiltig API-nyckel", + "invalid_user_key": "Ogiltig anv\u00e4ndarnyckel" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "API-nyckel" + }, + "title": "\u00c5terautenticera integration" + }, + "user": { + "data": { + "api_key": "API-nyckel", + "name": "Namn", + "user_key": "Anv\u00e4ndarnyckel" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Konfigurering av Pushover med YAML tas bort. \n\n Din befintliga YAML-konfiguration har automatiskt importerats till anv\u00e4ndargr\u00e4nssnittet. \n\n Ta bort Pushover YAML-konfigurationen fr\u00e5n filen configuration.yaml och starta om Home Assistant f\u00f6r att \u00e5tg\u00e4rda problemet.", + "title": "Pushover YAML-konfigurationen tas bort" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pvoutput/translations/es.json b/homeassistant/components/pvoutput/translations/es.json index 188a7e8d293..67a63360727 100644 --- a/homeassistant/components/pvoutput/translations/es.json +++ b/homeassistant/components/pvoutput/translations/es.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "error": { "cannot_connect": "No se pudo conectar", diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/bg.json b/homeassistant/components/pvpc_hourly_pricing/translations/bg.json new file mode 100644 index 00000000000..80a7cc489a9 --- /dev/null +++ b/homeassistant/components/pvpc_hourly_pricing/translations/bg.json @@ -0,0 +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" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pyload/sensor.py b/homeassistant/components/pyload/sensor.py index 81e1a02408a..be014a2a405 100644 --- a/homeassistant/components/pyload/sensor.py +++ b/homeassistant/components/pyload/sensor.py @@ -111,7 +111,7 @@ class PyLoadSensor(SensorEntity): """Return the unit of measurement of this entity, if any.""" return self._unit_of_measurement - def update(self): + def update(self) -> None: """Update state of sensor.""" try: self.api.update() diff --git a/homeassistant/components/qbittorrent/sensor.py b/homeassistant/components/qbittorrent/sensor.py index 1b34bda7a44..151055a1688 100644 --- a/homeassistant/components/qbittorrent/sensor.py +++ b/homeassistant/components/qbittorrent/sensor.py @@ -113,7 +113,7 @@ class QBittorrentSensor(SensorEntity): self._attr_name = f"{client_name} {description.name}" self._attr_available = False - def update(self): + def update(self) -> None: """Get the latest data from qBittorrent and updates the state.""" try: data = self.client.sync_main_data() diff --git a/homeassistant/components/qingping/translations/bg.json b/homeassistant/components/qingping/translations/bg.json new file mode 100644 index 00000000000..af9a13197df --- /dev/null +++ b/homeassistant/components/qingping/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/qingping/translations/cs.json b/homeassistant/components/qingping/translations/cs.json new file mode 100644 index 00000000000..925c1cbd337 --- /dev/null +++ b/homeassistant/components/qingping/translations/cs.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "already_in_progress": "Konfigurace ji\u017e prob\u00edh\u00e1", + "no_devices_found": "V s\u00edti nebyla nalezena \u017e\u00e1dn\u00e1 za\u0159\u00edzen\u00ed" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Chcete nastavit {name}?" + }, + "user": { + "data": { + "address": "Za\u0159\u00edzen\u00ed" + }, + "description": "Zvolte za\u0159\u00edzen\u00ed, kter\u00e9 chcete nastavit" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/qingping/translations/he.json b/homeassistant/components/qingping/translations/he.json new file mode 100644 index 00000000000..47308062d0d --- /dev/null +++ b/homeassistant/components/qingping/translations/he.json @@ -0,0 +1,16 @@ +{ + "config": { + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea {name}?" + }, + "user": { + "data": { + "address": "\u05d4\u05ea\u05e7\u05df" + }, + "description": "\u05d1\u05d7\u05d9\u05e8\u05ea \u05d4\u05ea\u05e7\u05df \u05dc\u05d4\u05d2\u05d3\u05e8\u05d4" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/qingping/translations/nl.json b/homeassistant/components/qingping/translations/nl.json new file mode 100644 index 00000000000..281d6feff46 --- /dev/null +++ b/homeassistant/components/qingping/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 is 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/qingping/translations/sv.json b/homeassistant/components/qingping/translations/sv.json new file mode 100644 index 00000000000..6c6f3f5f1bb --- /dev/null +++ b/homeassistant/components/qingping/translations/sv.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "already_in_progress": "Konfigurationsfl\u00f6det p\u00e5g\u00e5r redan", + "no_devices_found": "Inga enheter hittades i n\u00e4tverket", + "not_supported": "Enheten st\u00f6ds inte" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Vill du konfigurera {name}?" + }, + "user": { + "data": { + "address": "Enhet" + }, + "description": "V\u00e4lj en enhet att konfigurera" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/qnap/sensor.py b/homeassistant/components/qnap/sensor.py index 7366dc5dc41..8c093eb9232 100644 --- a/homeassistant/components/qnap/sensor.py +++ b/homeassistant/components/qnap/sensor.py @@ -330,7 +330,7 @@ class QNAPSensor(SensorEntity): ) return f"{server_name} {self.entity_description.name}" - def update(self): + def update(self) -> None: """Get the latest data for the states.""" self._api.update() diff --git a/homeassistant/components/qnap_qsw/translations/cs.json b/homeassistant/components/qnap_qsw/translations/cs.json index 33006d6761b..26d5197ced6 100644 --- a/homeassistant/components/qnap_qsw/translations/cs.json +++ b/homeassistant/components/qnap_qsw/translations/cs.json @@ -2,6 +2,24 @@ "config": { "abort": { "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed" + }, + "step": { + "discovered_connection": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + }, + "user": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/quantum_gateway/device_tracker.py b/homeassistant/components/quantum_gateway/device_tracker.py index b9be96ecaea..076c1d2722b 100644 --- a/homeassistant/components/quantum_gateway/device_tracker.py +++ b/homeassistant/components/quantum_gateway/device_tracker.py @@ -30,7 +30,9 @@ PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( ) -def get_scanner(hass: HomeAssistant, config: ConfigType) -> DeviceScanner | None: +def get_scanner( + hass: HomeAssistant, config: ConfigType +) -> QuantumGatewayDeviceScanner | None: """Validate the configuration and return a Quantum Gateway scanner.""" scanner = QuantumGatewayDeviceScanner(config[DOMAIN]) diff --git a/homeassistant/components/rachio/binary_sensor.py b/homeassistant/components/rachio/binary_sensor.py index 2fe99fb442e..294931b7538 100644 --- a/homeassistant/components/rachio/binary_sensor.py +++ b/homeassistant/components/rachio/binary_sensor.py @@ -119,7 +119,7 @@ class RachioControllerOnlineBinarySensor(RachioControllerBinarySensor): self.async_write_ha_state() - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Subscribe to updates.""" self._state = self._controller.init_data[KEY_STATUS] == STATUS_ONLINE @@ -165,7 +165,7 @@ class RachioRainSensor(RachioControllerBinarySensor): self.async_write_ha_state() - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Subscribe to updates.""" self._state = self._controller.init_data[KEY_RAIN_SENSOR_TRIPPED] diff --git a/homeassistant/components/rachio/config_flow.py b/homeassistant/components/rachio/config_flow.py index 00f31003ba6..477abcb3694 100644 --- a/homeassistant/components/rachio/config_flow.py +++ b/homeassistant/components/rachio/config_flow.py @@ -90,6 +90,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): await self.async_set_unique_id( discovery_info.properties[zeroconf.ATTR_PROPERTIES_ID] ) + self._abort_if_unique_id_configured() return await self.async_step_user() @staticmethod diff --git a/homeassistant/components/rachio/switch.py b/homeassistant/components/rachio/switch.py index 227e8beaec3..5e91d339d0d 100644 --- a/homeassistant/components/rachio/switch.py +++ b/homeassistant/components/rachio/switch.py @@ -3,6 +3,7 @@ from abc import abstractmethod from contextlib import suppress from datetime import timedelta import logging +from typing import Any import voluptuous as vol @@ -237,15 +238,15 @@ class RachioStandbySwitch(RachioSwitch): self.async_write_ha_state() - def turn_on(self, **kwargs) -> None: + def turn_on(self, **kwargs: Any) -> None: """Put the controller in standby mode.""" self._controller.rachio.device.turn_off(self._controller.controller_id) - def turn_off(self, **kwargs) -> None: + def turn_off(self, **kwargs: Any) -> None: """Resume controller functionality.""" self._controller.rachio.device.turn_on(self._controller.controller_id) - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Subscribe to updates.""" if KEY_ON in self._controller.init_data: self._state = not self._controller.init_data[KEY_ON] @@ -309,17 +310,17 @@ class RachioRainDelay(RachioSwitch): self._cancel_update = None self.async_write_ha_state() - def turn_on(self, **kwargs) -> None: + def turn_on(self, **kwargs: Any) -> None: """Activate a 24 hour rain delay on the controller.""" self._controller.rachio.device.rain_delay(self._controller.controller_id, 86400) _LOGGER.debug("Starting rain delay for 24 hours") - def turn_off(self, **kwargs) -> None: + def turn_off(self, **kwargs: Any) -> None: """Resume controller functionality.""" self._controller.rachio.device.rain_delay(self._controller.controller_id, 0) _LOGGER.debug("Canceling rain delay") - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Subscribe to updates.""" if KEY_RAIN_DELAY in self._controller.init_data: self._state = self._controller.init_data[ @@ -416,7 +417,7 @@ class RachioZone(RachioSwitch): props[ATTR_ZONE_SLOPE] = "Steep" return props - def turn_on(self, **kwargs) -> None: + def turn_on(self, **kwargs: Any) -> None: """Start watering this zone.""" # Stop other zones first self.turn_off() @@ -436,7 +437,7 @@ class RachioZone(RachioSwitch): str(manual_run_time), ) - def turn_off(self, **kwargs) -> None: + def turn_off(self, **kwargs: Any) -> None: """Stop watering all zones.""" self._controller.stop_watering() @@ -464,7 +465,7 @@ class RachioZone(RachioSwitch): self.async_write_ha_state() - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Subscribe to updates.""" self._state = self.zone_id == self._current_schedule.get(KEY_ZONE_ID) @@ -519,7 +520,7 @@ class RachioSchedule(RachioSwitch): """Return whether the schedule is allowed to run.""" return self._schedule_enabled - def turn_on(self, **kwargs) -> None: + def turn_on(self, **kwargs: Any) -> None: """Start this schedule.""" self._controller.rachio.schedulerule.start(self._schedule_id) _LOGGER.debug( @@ -528,7 +529,7 @@ class RachioSchedule(RachioSwitch): self._controller.name, ) - def turn_off(self, **kwargs) -> None: + def turn_off(self, **kwargs: Any) -> None: """Stop watering all zones.""" self._controller.stop_watering() @@ -548,7 +549,7 @@ class RachioSchedule(RachioSwitch): self.async_write_ha_state() - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Subscribe to updates.""" self._state = self._schedule_id == self._current_schedule.get(KEY_SCHEDULE_ID) diff --git a/homeassistant/components/radarr/__init__.py b/homeassistant/components/radarr/__init__.py index 24377725bfc..5e32f64b7ad 100644 --- a/homeassistant/components/radarr/__init__.py +++ b/homeassistant/components/radarr/__init__.py @@ -1 +1,117 @@ -"""The radarr component.""" +"""The Radarr component.""" +from __future__ import annotations + +from aiopyarr.models.host_configuration import PyArrHostConfiguration +from aiopyarr.radarr_client import RadarrClient + +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + ATTR_SW_VERSION, + CONF_API_KEY, + CONF_PLATFORM, + CONF_URL, + CONF_VERIFY_SSL, + Platform, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.device_registry import DeviceEntryType +from homeassistant.helpers.entity import DeviceInfo, EntityDescription +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue +from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DEFAULT_NAME, DOMAIN +from .coordinator import ( + DiskSpaceDataUpdateCoordinator, + HealthDataUpdateCoordinator, + MoviesDataUpdateCoordinator, + RadarrDataUpdateCoordinator, + StatusDataUpdateCoordinator, +) + +PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR] + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up the Steam integration.""" + if SENSOR_DOMAIN not in config: + return True + + for entry in config[SENSOR_DOMAIN]: + if entry[CONF_PLATFORM] == DOMAIN: + async_create_issue( + hass, + DOMAIN, + "deprecated_yaml", + breaks_in_ha_version="2022.10.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + ) + + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Radarr from a config entry.""" + host_configuration = PyArrHostConfiguration( + api_token=entry.data[CONF_API_KEY], + verify_ssl=entry.data[CONF_VERIFY_SSL], + url=entry.data[CONF_URL], + ) + radarr = RadarrClient( + host_configuration=host_configuration, + session=async_get_clientsession(hass, entry.data[CONF_VERIFY_SSL]), + ) + coordinators: dict[str, RadarrDataUpdateCoordinator] = { + "status": StatusDataUpdateCoordinator(hass, host_configuration, radarr), + "disk_space": DiskSpaceDataUpdateCoordinator(hass, host_configuration, radarr), + "health": HealthDataUpdateCoordinator(hass, host_configuration, radarr), + "movie": MoviesDataUpdateCoordinator(hass, host_configuration, radarr), + } + for coordinator in coordinators.values(): + await coordinator.async_config_entry_first_refresh() + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinators + 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 RadarrEntity(CoordinatorEntity[RadarrDataUpdateCoordinator]): + """Defines a base Radarr entity.""" + + _attr_has_entity_name = True + coordinator: RadarrDataUpdateCoordinator + + def __init__( + self, + coordinator: RadarrDataUpdateCoordinator, + description: EntityDescription, + ) -> None: + """Create Radarr entity.""" + super().__init__(coordinator) + self.entity_description = description + self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{description.key}" + + @property + def device_info(self) -> DeviceInfo: + """Return device information about the Radarr instance.""" + device_info = DeviceInfo( + configuration_url=self.coordinator.host_configuration.url, + entry_type=DeviceEntryType.SERVICE, + identifiers={(DOMAIN, self.coordinator.config_entry.entry_id)}, + manufacturer=DEFAULT_NAME, + name=self.coordinator.config_entry.title, + ) + if isinstance(self.coordinator, StatusDataUpdateCoordinator): + device_info[ATTR_SW_VERSION] = self.coordinator.data.version + return device_info diff --git a/homeassistant/components/radarr/binary_sensor.py b/homeassistant/components/radarr/binary_sensor.py new file mode 100644 index 00000000000..2a1a729e6f4 --- /dev/null +++ b/homeassistant/components/radarr/binary_sensor.py @@ -0,0 +1,41 @@ +"""Support for Radarr binary sensors.""" +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 . import RadarrEntity +from .const import DOMAIN, HEALTH_ISSUES + +BINARY_SENSOR_TYPE = BinarySensorEntityDescription( + key="health", + name="Health", + entity_category=EntityCategory.DIAGNOSTIC, + device_class=BinarySensorDeviceClass.PROBLEM, +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Radarr sensors based on a config entry.""" + coordinator = hass.data[DOMAIN][entry.entry_id]["health"] + async_add_entities([RadarrBinarySensor(coordinator, BINARY_SENSOR_TYPE)]) + + +class RadarrBinarySensor(RadarrEntity, BinarySensorEntity): + """Implementation of a Radarr binary sensor.""" + + @property + def is_on(self) -> bool: + """Return True if the entity is on.""" + return any(report.source in HEALTH_ISSUES for report in self.coordinator.data) diff --git a/homeassistant/components/radarr/config_flow.py b/homeassistant/components/radarr/config_flow.py new file mode 100644 index 00000000000..c37eeba4969 --- /dev/null +++ b/homeassistant/components/radarr/config_flow.py @@ -0,0 +1,148 @@ +"""Config flow for Radarr.""" +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any + +from aiohttp import ClientConnectorError +from aiopyarr import exceptions +from aiopyarr.models.host_configuration import PyArrHostConfiguration +from aiopyarr.radarr_client import RadarrClient +import voluptuous as vol + +from homeassistant.config_entries import ConfigEntry, ConfigFlow +from homeassistant.const import ( + CONF_API_KEY, + CONF_HOST, + CONF_PORT, + CONF_SSL, + CONF_URL, + CONF_VERIFY_SSL, +) +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import DEFAULT_NAME, DEFAULT_URL, DOMAIN, LOGGER + + +class RadarrConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for Radarr.""" + + VERSION = 1 + + def __init__(self) -> None: + """Initialize the flow.""" + self.entry: ConfigEntry | None = None + + async def async_step_reauth(self, _: Mapping[str, Any]) -> FlowResult: + """Handle configuration by re-auth.""" + self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) + + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: dict[str, str] | None = None + ) -> FlowResult: + """Confirm reauth dialog.""" + if user_input is not None: + return await self.async_step_user() + + self._set_confirm_only() + return self.async_show_form(step_id="reauth_confirm") + + 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 None: + user_input = dict(self.entry.data) if self.entry else None + + else: + try: + if result := await validate_input(self.hass, user_input): + user_input[CONF_API_KEY] = result[1] + except exceptions.ArrAuthenticationException: + errors = {"base": "invalid_auth"} + except (ClientConnectorError, exceptions.ArrConnectionException): + errors = {"base": "cannot_connect"} + except exceptions.ArrWrongAppException: + errors = {"base": "wrong_app"} + except exceptions.ArrZeroConfException: + errors = {"base": "zeroconf_failed"} + except exceptions.ArrException: + errors = {"base": "unknown"} + if not errors: + if self.entry: + self.hass.config_entries.async_update_entry( + self.entry, data=user_input + ) + await self.hass.config_entries.async_reload(self.entry.entry_id) + + return self.async_abort(reason="reauth_successful") + + return self.async_create_entry( + title=DEFAULT_NAME, + data=user_input, + ) + + user_input = user_input or {} + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required( + CONF_URL, default=user_input.get(CONF_URL, DEFAULT_URL) + ): str, + vol.Optional(CONF_API_KEY): str, + vol.Optional( + CONF_VERIFY_SSL, + default=user_input.get(CONF_VERIFY_SSL, False), + ): bool, + } + ), + errors=errors, + ) + + async def async_step_import(self, config: dict[str, Any]) -> FlowResult: + """Import a config entry from configuration.yaml.""" + for entry in self._async_current_entries(): + if entry.data[CONF_API_KEY] == config[CONF_API_KEY]: + _part = config[CONF_API_KEY][0:4] + _msg = f"Radarr yaml config with partial key {_part} has been imported. Please remove it" + LOGGER.warning(_msg) + return self.async_abort(reason="already_configured") + proto = "https" if config[CONF_SSL] else "http" + host_port = f"{config[CONF_HOST]}:{config[CONF_PORT]}" + path = "" + if config["urlbase"].rstrip("/") not in ("", "/", "/api"): + path = config["urlbase"].rstrip("/") + return self.async_create_entry( + title=DEFAULT_NAME, + data={ + CONF_URL: f"{proto}://{host_port}{path}", + CONF_API_KEY: config[CONF_API_KEY], + CONF_VERIFY_SSL: False, + }, + ) + + +async def validate_input( + hass: HomeAssistant, data: dict[str, Any] +) -> tuple[str, str, str] | None: + """Validate the user input allows us to connect.""" + host_configuration = PyArrHostConfiguration( + api_token=data.get(CONF_API_KEY, ""), + verify_ssl=data[CONF_VERIFY_SSL], + url=data[CONF_URL], + ) + radarr = RadarrClient( + host_configuration=host_configuration, + session=async_get_clientsession(hass), + ) + if CONF_API_KEY not in data: + return await radarr.async_try_zeroconf() + await radarr.async_get_system_status() + return None diff --git a/homeassistant/components/radarr/const.py b/homeassistant/components/radarr/const.py new file mode 100644 index 00000000000..b77e134ca34 --- /dev/null +++ b/homeassistant/components/radarr/const.py @@ -0,0 +1,18 @@ +"""Constants for Radarr.""" +import logging +from typing import Final + +DOMAIN: Final = "radarr" + +# Defaults +DEFAULT_NAME = "Radarr" +DEFAULT_URL = "http://127.0.0.1:7878" + +HEALTH_ISSUES = ( + "DownloadClientCheck", + "DownloadClientStatusCheck", + "IndexerRssCheck", + "IndexerSearchCheck", +) + +LOGGER = logging.getLogger(__package__) diff --git a/homeassistant/components/radarr/coordinator.py b/homeassistant/components/radarr/coordinator.py new file mode 100644 index 00000000000..06ea32e790f --- /dev/null +++ b/homeassistant/components/radarr/coordinator.py @@ -0,0 +1,90 @@ +"""Data update coordinator for the Radarr integration.""" +from __future__ import annotations + +from abc import abstractmethod +from datetime import timedelta +from typing import Generic, TypeVar, cast + +from aiopyarr import Health, RootFolder, SystemStatus, exceptions +from aiopyarr.models.host_configuration import PyArrHostConfiguration +from aiopyarr.radarr_client import RadarrClient + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import DOMAIN, LOGGER + +T = TypeVar("T", SystemStatus, list[RootFolder], list[Health], int) + + +class RadarrDataUpdateCoordinator(DataUpdateCoordinator, Generic[T]): + """Data update coordinator for the Radarr integration.""" + + config_entry: ConfigEntry + + def __init__( + self, + hass: HomeAssistant, + host_configuration: PyArrHostConfiguration, + api_client: RadarrClient, + ) -> None: + """Initialize the coordinator.""" + super().__init__( + hass=hass, + logger=LOGGER, + name=DOMAIN, + update_interval=timedelta(seconds=30), + ) + self.api_client = api_client + self.host_configuration = host_configuration + + async def _async_update_data(self) -> T: + """Get the latest data from Radarr.""" + try: + return await self._fetch_data() + + except exceptions.ArrConnectionException as ex: + raise UpdateFailed(ex) from ex + except exceptions.ArrAuthenticationException as ex: + raise ConfigEntryAuthFailed( + "API Key is no longer valid. Please reauthenticate" + ) from ex + + @abstractmethod + async def _fetch_data(self) -> T: + """Fetch the actual data.""" + raise NotImplementedError + + +class StatusDataUpdateCoordinator(RadarrDataUpdateCoordinator): + """Status update coordinator for Radarr.""" + + async def _fetch_data(self) -> SystemStatus: + """Fetch the data.""" + return await self.api_client.async_get_system_status() + + +class DiskSpaceDataUpdateCoordinator(RadarrDataUpdateCoordinator): + """Disk space update coordinator for Radarr.""" + + async def _fetch_data(self) -> list[RootFolder]: + """Fetch the data.""" + return cast(list, await self.api_client.async_get_root_folders()) + + +class HealthDataUpdateCoordinator(RadarrDataUpdateCoordinator): + """Health update coordinator.""" + + async def _fetch_data(self) -> list[Health]: + """Fetch the health data.""" + return await self.api_client.async_get_failed_health_checks() + + +class MoviesDataUpdateCoordinator(RadarrDataUpdateCoordinator): + """Movies update coordinator.""" + + async def _fetch_data(self) -> int: + """Fetch the movies data.""" + return len(cast(list, await self.api_client.async_get_movies())) diff --git a/homeassistant/components/radarr/manifest.json b/homeassistant/components/radarr/manifest.json index 611b4a33f3b..5bc15b24069 100644 --- a/homeassistant/components/radarr/manifest.json +++ b/homeassistant/components/radarr/manifest.json @@ -2,6 +2,9 @@ "domain": "radarr", "name": "Radarr", "documentation": "https://www.home-assistant.io/integrations/radarr", - "codeowners": [], - "iot_class": "local_polling" + "requirements": ["aiopyarr==22.9.0"], + "codeowners": ["@tkdrob"], + "config_flow": true, + "iot_class": "local_polling", + "loggers": ["aiopyarr"] } diff --git a/homeassistant/components/radarr/sensor.py b/homeassistant/components/radarr/sensor.py index c0c10c5b1b3..e424844c602 100644 --- a/homeassistant/components/radarr/sensor.py +++ b/homeassistant/components/radarr/sensor.py @@ -1,20 +1,22 @@ """Support for Radarr.""" from __future__ import annotations -from datetime import datetime, timedelta -from http import HTTPStatus -import logging -import time -from typing import Any +from collections.abc import Callable +from copy import deepcopy +from dataclasses import dataclass +from datetime import timezone +from typing import Generic -import requests +from aiopyarr import Diskspace, RootFolder import voluptuous as vol from homeassistant.components.sensor import ( PLATFORM_SCHEMA, + SensorDeviceClass, SensorEntity, SensorEntityDescription, ) +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( CONF_API_KEY, CONF_HOST, @@ -22,245 +24,168 @@ from homeassistant.const import ( CONF_PORT, CONF_SSL, DATA_BYTES, - DATA_EXABYTES, DATA_GIGABYTES, DATA_KILOBYTES, DATA_MEGABYTES, - DATA_PETABYTES, - DATA_TERABYTES, - DATA_YOTTABYTES, - DATA_ZETTABYTES, ) from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from homeassistant.util import dt as dt_util +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType -_LOGGER = logging.getLogger(__name__) +from . import RadarrEntity +from .const import DOMAIN +from .coordinator import RadarrDataUpdateCoordinator, T -CONF_DAYS = "days" -CONF_INCLUDED = "include_paths" -CONF_UNIT = "unit" -CONF_URLBASE = "urlbase" -DEFAULT_HOST = "localhost" -DEFAULT_PORT = 7878 -DEFAULT_URLBASE = "" -DEFAULT_DAYS = "1" -DEFAULT_UNIT = DATA_GIGABYTES +def get_space(data: list[Diskspace], name: str) -> str: + """Get space.""" + space = [ + mount.freeSpace / 1024 ** BYTE_SIZES.index(DATA_GIGABYTES) + for mount in data + if name in mount.path + ] + return f"{space[0]:.2f}" -SCAN_INTERVAL = timedelta(minutes=10) -SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( - SensorEntityDescription( - key="diskspace", - name="Disk Space", +def get_modified_description( + description: RadarrSensorEntityDescription, mount: RootFolder +) -> tuple[RadarrSensorEntityDescription, str]: + """Return modified description and folder name.""" + desc = deepcopy(description) + name = mount.path.rsplit("/")[-1].rsplit("\\")[-1] + desc.key = f"{description.key}_{name}" + desc.name = f"{description.name} {name}".capitalize() + return desc, name + + +@dataclass +class RadarrSensorEntityDescriptionMixIn(Generic[T]): + """Mixin for required keys.""" + + value_fn: Callable[[T, str], str] + + +@dataclass +class RadarrSensorEntityDescription( + SensorEntityDescription, RadarrSensorEntityDescriptionMixIn[T], Generic[T] +): + """Class to describe a Radarr sensor.""" + + description_fn: Callable[ + [RadarrSensorEntityDescription, RootFolder], + tuple[RadarrSensorEntityDescription, str] | None, + ] = lambda _, __: None + + +SENSOR_TYPES: dict[str, RadarrSensorEntityDescription] = { + "disk_space": RadarrSensorEntityDescription( + key="disk_space", + name="Disk space", native_unit_of_measurement=DATA_GIGABYTES, icon="mdi:harddisk", + value_fn=get_space, + description_fn=get_modified_description, ), - SensorEntityDescription( - key="upcoming", - name="Upcoming", - native_unit_of_measurement="Movies", - icon="mdi:television", - ), - SensorEntityDescription( + "movie": RadarrSensorEntityDescription( key="movies", name="Movies", native_unit_of_measurement="Movies", icon="mdi:television", + entity_registry_enabled_default=False, + value_fn=lambda data, _: data, ), - SensorEntityDescription( - key="commands", - name="Commands", - native_unit_of_measurement="Commands", - icon="mdi:code-braces", + "status": RadarrSensorEntityDescription( + key="start_time", + name="Start time", + device_class=SensorDeviceClass.TIMESTAMP, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + value_fn=lambda data, _: data.startTime.replace(tzinfo=timezone.utc), ), - SensorEntityDescription( - key="status", - name="Status", - native_unit_of_measurement="Status", - icon="mdi:information", - ), -) - -SENSOR_KEYS: list[str] = [desc.key for desc in SENSOR_TYPES] - -ENDPOINTS = { - "diskspace": "{0}://{1}:{2}/{3}api/diskspace", - "upcoming": "{0}://{1}:{2}/{3}api/calendar?start={4}&end={5}", - "movies": "{0}://{1}:{2}/{3}api/movie", - "commands": "{0}://{1}:{2}/{3}api/command", - "status": "{0}://{1}:{2}/{3}api/system/status", } -# Support to Yottabytes for the future, why not BYTE_SIZES = [ DATA_BYTES, DATA_KILOBYTES, DATA_MEGABYTES, DATA_GIGABYTES, - DATA_TERABYTES, - DATA_PETABYTES, - DATA_EXABYTES, - DATA_ZETTABYTES, - DATA_YOTTABYTES, ] +# Deprecated in Home Assistant 2022.10 PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_API_KEY): cv.string, - vol.Optional(CONF_DAYS, default=DEFAULT_DAYS): cv.string, - vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, - vol.Optional(CONF_INCLUDED, default=[]): cv.ensure_list, + vol.Optional("days", default=1): cv.string, + vol.Optional(CONF_HOST, default="localhost"): cv.string, + vol.Optional("include_paths", default=[]): cv.ensure_list, vol.Optional(CONF_MONITORED_CONDITIONS, default=["movies"]): vol.All( - cv.ensure_list, [vol.In(SENSOR_KEYS)] + cv.ensure_list ), - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_PORT, default=7878): cv.port, vol.Optional(CONF_SSL, default=False): cv.boolean, - vol.Optional(CONF_UNIT, default=DEFAULT_UNIT): vol.In(BYTE_SIZES), - vol.Optional(CONF_URLBASE, default=DEFAULT_URLBASE): cv.string, + vol.Optional("unit", default=DATA_GIGABYTES): cv.string, + vol.Optional("urlbase", default=""): cv.string, } ) +PARALLEL_UPDATES = 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 Radarr platform.""" - conditions = config[CONF_MONITORED_CONDITIONS] - # deprecated in 2022.3 - entities = [ - RadarrSensor(hass, config, description) - for description in SENSOR_TYPES - if description.key in conditions + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=config + ) + ) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Radarr sensors based on a config entry.""" + coordinators: dict[str, RadarrDataUpdateCoordinator] = hass.data[DOMAIN][ + entry.entry_id ] - add_entities(entities, True) + entities = [] + for coordinator_type, description in SENSOR_TYPES.items(): + coordinator = coordinators[coordinator_type] + if coordinator_type != "disk_space": + entities.append(RadarrSensor(coordinator, description)) + else: + entities.extend( + RadarrSensor(coordinator, *get_modified_description(description, mount)) + for mount in coordinator.data + if description.description_fn + ) + async_add_entities(entities) -class RadarrSensor(SensorEntity): +class RadarrSensor(RadarrEntity, SensorEntity): """Implementation of the Radarr sensor.""" - def __init__(self, hass, conf, description: SensorEntityDescription): - """Create Radarr entity.""" - self.entity_description = description + coordinator: RadarrDataUpdateCoordinator + entity_description: RadarrSensorEntityDescription - self.conf = conf - self.host = conf.get(CONF_HOST) - self.port = conf.get(CONF_PORT) - self.urlbase = conf.get(CONF_URLBASE) - if self.urlbase: - self.urlbase = f"{self.urlbase.strip('/')}/" - self.apikey = conf.get(CONF_API_KEY) - self.included = conf.get(CONF_INCLUDED) - self.days = int(conf.get(CONF_DAYS)) - self.ssl = "https" if conf.get(CONF_SSL) else "http" - self.data: list[Any] = [] - self._attr_name = f"Radarr {description.name}" - if description.key == "diskspace": - self._attr_native_unit_of_measurement = conf.get(CONF_UNIT) - self._attr_available = False + def __init__( + self, + coordinator: RadarrDataUpdateCoordinator, + description: RadarrSensorEntityDescription, + folder_name: str = "", + ) -> None: + """Create Radarr entity.""" + super().__init__(coordinator, description) + self.folder_name = folder_name @property - def extra_state_attributes(self): - """Return the state attributes of the sensor.""" - attributes = {} - sensor_type = self.entity_description.key - if sensor_type == "upcoming": - for movie in self.data: - attributes[to_key(movie)] = get_release_date(movie) - elif sensor_type == "commands": - for command in self.data: - attributes[command["name"]] = command["state"] - elif sensor_type == "diskspace": - for data in self.data: - free_space = to_unit(data["freeSpace"], self.native_unit_of_measurement) - total_space = to_unit( - data["totalSpace"], self.native_unit_of_measurement - ) - percentage_used = ( - 0 if total_space == 0 else free_space / total_space * 100 - ) - attributes[data["path"]] = "{:.2f}/{:.2f}{} ({:.2f}%)".format( - free_space, - total_space, - self.native_unit_of_measurement, - percentage_used, - ) - elif sensor_type == "movies": - for movie in self.data: - attributes[to_key(movie)] = movie["downloaded"] - elif sensor_type == "status": - attributes = self.data - - return attributes - - def update(self): - """Update the data for the sensor.""" - sensor_type = self.entity_description.key - time_zone = dt_util.get_time_zone(self.hass.config.time_zone) - start = get_date(time_zone) - end = get_date(time_zone, self.days) - try: - res = requests.get( - ENDPOINTS[sensor_type].format( - self.ssl, self.host, self.port, self.urlbase, start, end - ), - headers={"X-Api-Key": self.apikey}, - timeout=10, - ) - except OSError: - _LOGGER.warning("Host %s is not available", self.host) - self._attr_available = False - self._attr_native_value = None - return - - if res.status_code == HTTPStatus.OK: - if sensor_type in ("upcoming", "movies", "commands"): - self.data = res.json() - self._attr_native_value = len(self.data) - elif sensor_type == "diskspace": - # If included paths are not provided, use all data - if self.included == []: - self.data = res.json() - else: - # Filter to only show lists that are included - self.data = list( - filter(lambda x: x["path"] in self.included, res.json()) - ) - self._attr_native_value = "{:.2f}".format( - to_unit( - sum(data["freeSpace"] for data in self.data), - self.native_unit_of_measurement, - ) - ) - elif sensor_type == "status": - self.data = res.json() - self._attr_native_value = self.data["version"] - self._attr_available = True - - -def get_date(zone, offset=0): - """Get date based on timezone and offset of days.""" - day = 60 * 60 * 24 - return datetime.date(datetime.fromtimestamp(time.time() + day * offset, tz=zone)) - - -def get_release_date(data): - """Get release date.""" - if not (date := data.get("physicalRelease")): - date = data.get("inCinemas") - return date - - -def to_key(data): - """Get key.""" - return "{} ({})".format(data["title"], data["year"]) - - -def to_unit(value, unit): - """Convert bytes to give unit.""" - return value / 1024 ** BYTE_SIZES.index(unit) + def native_value(self) -> StateType: + """Return the state of the sensor.""" + return self.entity_description.value_fn(self.coordinator.data, self.folder_name) diff --git a/homeassistant/components/radarr/strings.json b/homeassistant/components/radarr/strings.json new file mode 100644 index 00000000000..6fa9b64c2c8 --- /dev/null +++ b/homeassistant/components/radarr/strings.json @@ -0,0 +1,44 @@ +{ + "config": { + "step": { + "user": { + "description": "API key can be retrieved automatically if login credentials were not set in application.\nYour API key can be found in Settings > General in the Radarr Web UI.", + "data": { + "api_key": "[%key:common::config_flow::data::api_key%]", + "url": "[%key:common::config_flow::data::url%]", + "verify_ssl": "[%key:common::config_flow::data::verify_ssl%]" + } + }, + "reauth_confirm": { + "title": "[%key:common::config_flow::title::reauth%]", + "description": "The Radarr integration needs to be manually re-authenticated with the Radarr API" + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "zeroconf_failed": "API key not found. Please enter it manually", + "wrong_app": "Incorrect application reached. Please try again", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" + } + }, + "options": { + "step": { + "init": { + "data": { + "upcoming_days": "Number of upcoming days to display" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "title": "The Radarr YAML configuration is being removed", + "description": "Configuring Radarr using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the Radarr YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } + } +} diff --git a/homeassistant/components/radarr/translations/bg.json b/homeassistant/components/radarr/translations/bg.json new file mode 100644 index 00000000000..5ce0eec5412 --- /dev/null +++ b/homeassistant/components/radarr/translations/bg.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u043b\u0443\u0433\u0430\u0442\u0430 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0430", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "reauth_confirm": { + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u043d\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430" + }, + "user": { + "data": { + "api_key": "API \u043a\u043b\u044e\u0447", + "url": "URL" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "upcoming_days": "\u0411\u0440\u043e\u0439 \u043f\u0440\u0435\u0434\u0441\u0442\u043e\u044f\u0449\u0438 \u0434\u043d\u0438 \u0437\u0430 \u043f\u043e\u043a\u0430\u0437\u0432\u0430\u043d\u0435" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/radarr/translations/ca.json b/homeassistant/components/radarr/translations/ca.json new file mode 100644 index 00000000000..c94cd1cd901 --- /dev/null +++ b/homeassistant/components/radarr/translations/ca.json @@ -0,0 +1,48 @@ +{ + "config": { + "abort": { + "already_configured": "El servei 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", + "unknown": "Error inesperat", + "wrong_app": "No s'ha trobat l'aplicaci\u00f3 correcta. Torna-ho a intentar", + "zeroconf_failed": "No s'ha trobat la clau API. Introdueix-la manualment" + }, + "step": { + "reauth_confirm": { + "description": "La integraci\u00f3 Radarr ha de tornar a autenticar-se manualment amb l'API de Radarr", + "title": "Reautenticaci\u00f3 de la integraci\u00f3" + }, + "user": { + "data": { + "api_key": "Clau API", + "url": "URL", + "verify_ssl": "Verifica el certificat SSL" + }, + "description": "La clau API es pot obtenir autom\u00e0ticament si les credencials d'inici de sessi\u00f3 no s'han establert a l'aplicaci\u00f3.\nLa teva clau API es pot trobar a Configuraci\u00f3 ('Settings') > General, a la interf\u00edcie web de Radarr." + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "La configuraci\u00f3 de Radarr mitjan\u00e7ant YAML s'eliminar\u00e0 de Home Assistant.\n\nLa configuraci\u00f3 YAML existent s'ha importat autom\u00e0ticament a la interf\u00edcie d'usuari.\n\nElimina la configuraci\u00f3 YAML de Radarr del fitxer configuration.yaml i reinicia Home Assistant per solucionar aquest problema.", + "title": "La configuraci\u00f3 YAML de Radarr est\u00e0 sent eliminada" + }, + "removed_attributes": { + "description": "Per precauci\u00f3, s'han fet alguns canvis importants en el sensor recompte de pel\u00b7l\u00edcules.\n\nAquest sensor pot causar problemes amb bases de dades molt grans. Si encara vols utilitzar-lo, pots fer-ho. \n\nEls noms de pel\u00b7l\u00edcules ja no s'inclouen com a atributs al sensor pel\u00b7l\u00edcules. \n\nPropers s'ha eliminat. S'ha modernitzat com a elements de calendari. L'espai del disc ara es divideix en diferents sensors, un per a cada carpeta. \n\nL'estat i les comandes s'han eliminat perqu\u00e8 no sembla que tinguin un valor real per a les automatitzacions.", + "title": "Canvis a la integraci\u00f3 Radarr" + } + }, + "options": { + "step": { + "init": { + "data": { + "upcoming_days": "Nombre de dies propers a mostrar" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/radarr/translations/de.json b/homeassistant/components/radarr/translations/de.json new file mode 100644 index 00000000000..81aafd0a351 --- /dev/null +++ b/homeassistant/components/radarr/translations/de.json @@ -0,0 +1,48 @@ +{ + "config": { + "abort": { + "already_configured": "Der Dienst ist bereits konfiguriert", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "unknown": "Unerwarteter Fehler", + "wrong_app": "Falsche Anwendung erreicht. Bitte versuche es erneut", + "zeroconf_failed": "API-Schl\u00fcssel nicht gefunden. Bitte gib ihn manuell ein" + }, + "step": { + "reauth_confirm": { + "description": "Die Radarr-Integration muss manuell erneut mit der Radarr-API authentifiziert werden", + "title": "Integration erneut authentifizieren" + }, + "user": { + "data": { + "api_key": "API-Schl\u00fcssel", + "url": "URL", + "verify_ssl": "SSL-Zertifikat \u00fcberpr\u00fcfen" + }, + "description": "Der API-Schl\u00fcssel kann automatisch abgerufen werden, wenn in der Anwendung keine Anmeldeinformationen festgelegt wurden.\nDeinen API-Schl\u00fcssel findest unter Einstellungen > Allgemein in der Radarr-Web-Benutzeroberfl\u00e4che." + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Die Konfiguration von Radarr mit YAML wird entfernt. \n\nDeine vorhandene YAML-Konfiguration wurde automatisch in die Benutzeroberfl\u00e4che importiert. \n\nEntferne die Radarr-YAML-Konfiguration aus deiner configuration.yaml-Datei und starte Home Assistant neu, um dieses Problem zu beheben.", + "title": "Die Radarr-YAML-Konfiguration wird entfernt" + }, + "removed_attributes": { + "description": "Es wurden einige \u00c4nderungen vorgenommen, um den Filmz\u00e4hlsensor aus Vorsicht zu deaktivieren.\n\nDieser Sensor kann bei gro\u00dfen Datenbanken Probleme verursachen. Wenn du ihn dennoch verwenden m\u00f6chtest, kannst du dies tun.\n\nFilmnamen werden nicht mehr als Attribute in den Filmsensor aufgenommen.\n\nUpcoming wurde entfernt. Er wird modernisiert, so wie es bei Kalenderelementen sein sollte. Der Speicherplatz ist jetzt in verschiedene Sensoren aufgeteilt, einen f\u00fcr jeden Ordner.\n\nStatus und Befehle wurden entfernt, da sie f\u00fcr Automatisierungen keinen wirklichen Wert zu haben scheinen.", + "title": "\u00c4nderungen an der Radarr-Integration" + } + }, + "options": { + "step": { + "init": { + "data": { + "upcoming_days": "Anzahl der anzuzeigenden Tage" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/radarr/translations/el.json b/homeassistant/components/radarr/translations/el.json new file mode 100644 index 00000000000..f7eb031cba5 --- /dev/null +++ b/homeassistant/components/radarr/translations/el.json @@ -0,0 +1,48 @@ +{ + "config": { + "abort": { + "already_configured": "\u0397 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af", + "reauth_successful": "\u039f \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2" + }, + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1", + "wrong_app": "\u0395\u03c0\u03af\u03c4\u03b5\u03c5\u03be\u03b7 \u03bb\u03b1\u03bd\u03b8\u03b1\u03c3\u03bc\u03ad\u03bd\u03b7\u03c2 \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae\u03c2. \u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03ce \u03b4\u03bf\u03ba\u03b9\u03bc\u03ac\u03c3\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac", + "zeroconf_failed": "\u03a4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API \u03b4\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b5. \u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03ce \u03b5\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf \u03c7\u03b5\u03b9\u03c1\u03bf\u03ba\u03af\u03bd\u03b7\u03c4\u03b1" + }, + "step": { + "reauth_confirm": { + "description": "\u0397 \u03b5\u03bd\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03c4\u03bf\u03c5 Radarr \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03c0\u03b1\u03bb\u03b7\u03b8\u03b5\u03c5\u03c4\u03b5\u03af \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03bc\u03b5 \u03bc\u03b7 \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03bf \u03c4\u03c1\u03cc\u03c0\u03bf \u03bc\u03b5 \u03c4\u03bf Radarr API", + "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", + "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" + }, + "description": "\u03a4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03b1\u03bd\u03b1\u03ba\u03c4\u03b7\u03b8\u03b5\u03af \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b1 \u03b5\u03ac\u03bd \u03c4\u03b1 \u03b4\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03ae\u03c1\u03b9\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03b4\u03b5\u03bd \u03b5\u03af\u03c7\u03b1\u03bd \u03bf\u03c1\u03b9\u03c3\u03c4\u03b5\u03af \u03c3\u03c4\u03b7\u03bd \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae.\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 API \u03c3\u03b1\u03c2 \u03c3\u03c4\u03b9\u03c2 \u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 > \u0393\u03b5\u03bd\u03b9\u03ba\u03ac \u03c3\u03c4\u03bf \u03c0\u03b5\u03c1\u03b9\u03b2\u03ac\u03bb\u03bb\u03bf\u03bd \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7 \u03c4\u03bf\u03c5 Radarr Web." + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Radarr \u03bc\u03b5 \u03c7\u03c1\u03ae\u03c3\u03b7 YAML \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9. \n\n \u0397 \u03c5\u03c0\u03ac\u03c1\u03c7\u03bf\u03c5\u03c3\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03ae \u03c3\u03b1\u03c2 YAML \u03ad\u03c7\u03b5\u03b9 \u03b5\u03b9\u03c3\u03b1\u03c7\u03b8\u03b5\u03af \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b1 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03c0\u03b1\u03c6\u03ae \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7. \n\n \u039a\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 Radarr YAML \u03b1\u03c0\u03cc \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf configuration.yaml \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf Home Assistant \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1.", + "title": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 Radarr YAML \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9" + }, + "removed_attributes": { + "description": "\u0388\u03b3\u03b9\u03bd\u03b1\u03bd \u03bf\u03c1\u03b9\u03c3\u03bc\u03ad\u03bd\u03b5\u03c2 \u03c3\u03b7\u03bc\u03b1\u03bd\u03c4\u03b9\u03ba\u03ad\u03c2 \u03b1\u03bb\u03bb\u03b1\u03b3\u03ad\u03c2 \u03c3\u03c4\u03b7\u03bd \u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03bc\u03ad\u03c4\u03c1\u03b7\u03c3\u03b7\u03c2 \u03c4\u03b1\u03b9\u03bd\u03b9\u03ce\u03bd \u03c7\u03c9\u03c1\u03af\u03c2 \u03c0\u03c1\u03bf\u03c3\u03bf\u03c7\u03ae. \n\n \u0391\u03c5\u03c4\u03cc\u03c2 \u03bf \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1\u03c2 \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03c0\u03c1\u03bf\u03ba\u03b1\u03bb\u03ad\u03c3\u03b5\u03b9 \u03c0\u03c1\u03bf\u03b2\u03bb\u03ae\u03bc\u03b1\u03c4\u03b1 \u03bc\u03b5 \u03c4\u03b5\u03c1\u03ac\u03c3\u03c4\u03b9\u03b5\u03c2 \u03b2\u03ac\u03c3\u03b5\u03b9\u03c2 \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03c9\u03bd. \u0395\u03ac\u03bd \u03b5\u03be\u03b1\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03b5\u03af\u03c4\u03b5 \u03bd\u03b1 \u03b8\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c4\u03bf \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03c4\u03b5, \u03bc\u03c0\u03bf\u03c1\u03b5\u03af\u03c4\u03b5 \u03bd\u03b1 \u03c4\u03bf \u03ba\u03ac\u03bd\u03b5\u03c4\u03b5. \n\n \u03a4\u03b1 \u03bf\u03bd\u03cc\u03bc\u03b1\u03c4\u03b1 \u03c4\u03b1\u03b9\u03bd\u03b9\u03ce\u03bd \u03b4\u03b5\u03bd \u03c0\u03b5\u03c1\u03b9\u03bb\u03b1\u03bc\u03b2\u03ac\u03bd\u03bf\u03bd\u03c4\u03b1\u03b9 \u03c0\u03bb\u03ad\u03bf\u03bd \u03c9\u03c2 \u03c7\u03b1\u03c1\u03b1\u03ba\u03c4\u03b7\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03ac \u03c3\u03c4\u03bf\u03bd \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03c4\u03b1\u03b9\u03bd\u03b9\u03ce\u03bd. \n\n \u03a4\u03bf \u03b5\u03c0\u03b5\u03c1\u03c7\u03cc\u03bc\u03b5\u03bd\u03bf \u03ad\u03c7\u03b5\u03b9 \u03b1\u03c6\u03b1\u03b9\u03c1\u03b5\u03b8\u03b5\u03af. \u0395\u03ba\u03c3\u03c5\u03b3\u03c7\u03c1\u03bf\u03bd\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03cc\u03c0\u03c9\u03c2 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03c4\u03b1 \u03c3\u03c4\u03bf\u03b9\u03c7\u03b5\u03af\u03b1 \u03c4\u03bf\u03c5 \u03b7\u03bc\u03b5\u03c1\u03bf\u03bb\u03bf\u03b3\u03af\u03bf\u03c5. \u039f \u03c7\u03ce\u03c1\u03bf\u03c2 \u03c3\u03c4\u03bf \u03b4\u03af\u03c3\u03ba\u03bf \u03c7\u03c9\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03c0\u03bb\u03ad\u03bf\u03bd \u03c3\u03b5 \u03b4\u03b9\u03b1\u03c6\u03bf\u03c1\u03b5\u03c4\u03b9\u03ba\u03bf\u03cd\u03c2 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b5\u03c2, \u03ad\u03bd\u03b1\u03bd \u03b3\u03b9\u03b1 \u03ba\u03ac\u03b8\u03b5 \u03c6\u03ac\u03ba\u03b5\u03bb\u03bf. \n\n \u0397 \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7 \u03ba\u03b1\u03b9 \u03bf\u03b9 \u03b5\u03bd\u03c4\u03bf\u03bb\u03ad\u03c2 \u03ad\u03c7\u03bf\u03c5\u03bd \u03b1\u03c6\u03b1\u03b9\u03c1\u03b5\u03b8\u03b5\u03af \u03ba\u03b1\u03b8\u03ce\u03c2 \u03b4\u03b5\u03bd \u03c6\u03b1\u03af\u03bd\u03b5\u03c4\u03b1\u03b9 \u03bd\u03b1 \u03ad\u03c7\u03bf\u03c5\u03bd \u03c0\u03c1\u03b1\u03b3\u03bc\u03b1\u03c4\u03b9\u03ba\u03ae \u03b1\u03be\u03af\u03b1 \u03b3\u03b9\u03b1 \u03c4\u03bf\u03c5\u03c2 \u03b1\u03c5\u03c4\u03bf\u03bc\u03b1\u03c4\u03b9\u03c3\u03bc\u03bf\u03cd\u03c2.", + "title": "\u0391\u03bb\u03bb\u03b1\u03b3\u03ad\u03c2 \u03c3\u03c4\u03b7\u03bd \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Radarr" + } + }, + "options": { + "step": { + "init": { + "data": { + "upcoming_days": "\u0391\u03c1\u03b9\u03b8\u03bc\u03cc\u03c2 \u03c0\u03c1\u03bf\u03c3\u03b5\u03c7\u03ce\u03bd \u03b7\u03bc\u03b5\u03c1\u03ce\u03bd \u03c0\u03c1\u03bf\u03c2 \u03b5\u03bc\u03c6\u03ac\u03bd\u03b9\u03c3\u03b7" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/radarr/translations/en.json b/homeassistant/components/radarr/translations/en.json new file mode 100644 index 00000000000..168c3cc2fe2 --- /dev/null +++ b/homeassistant/components/radarr/translations/en.json @@ -0,0 +1,48 @@ +{ + "config": { + "abort": { + "already_configured": "Service is already configured", + "reauth_successful": "Re-authentication was successful" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error", + "wrong_app": "Incorrect application reached. Please try again", + "zeroconf_failed": "API key not found. Please enter it manually" + }, + "step": { + "reauth_confirm": { + "description": "The Radarr integration needs to be manually re-authenticated with the Radarr API", + "title": "Reauthenticate Integration" + }, + "user": { + "data": { + "api_key": "API Key", + "url": "URL", + "verify_ssl": "Verify SSL certificate" + }, + "description": "API key can be retrieved automatically if login credentials were not set in application.\nYour API key can be found in Settings > General in the Radarr Web UI." + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Configuring Radarr using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the Radarr YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "title": "The Radarr YAML configuration is being removed" + }, + "removed_attributes": { + "description": "Some breaking changes has been made in disabling the Movies count sensor out of caution.\n\nThis sensor can cause problems with massive databases. If you still wish to use it, you may do so.\n\nMovie names are no longer included as attributes in the movies sensor.\n\nUpcoming has been removed. It is being modernized as calendar items should be. Disk space is now split into different sensors, one for each folder.\n\nStatus and commands have been removed as they don't appear to have real value for automations.", + "title": "Changes to the Radarr integration" + } + }, + "options": { + "step": { + "init": { + "data": { + "upcoming_days": "Number of upcoming days to display" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/radarr/translations/es.json b/homeassistant/components/radarr/translations/es.json new file mode 100644 index 00000000000..5f88c7ef8f4 --- /dev/null +++ b/homeassistant/components/radarr/translations/es.json @@ -0,0 +1,48 @@ +{ + "config": { + "abort": { + "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", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "unknown": "Error inesperado", + "wrong_app": "Se ha alcanzado una aplicaci\u00f3n incorrecta. Por favor, int\u00e9ntalo de nuevo", + "zeroconf_failed": "Clave API no encontrada. Por favor, introd\u00facela manualmente" + }, + "step": { + "reauth_confirm": { + "description": "La integraci\u00f3n Radarr debe volver a autenticarse manualmente con la API de Radarr", + "title": "Volver a autenticar la integraci\u00f3n" + }, + "user": { + "data": { + "api_key": "Clave API", + "url": "URL", + "verify_ssl": "Verificar el certificado SSL" + }, + "description": "La clave API se puede recuperar autom\u00e1ticamente si las credenciales de inicio de sesi\u00f3n no se configuraron en la aplicaci\u00f3n.\nPuedes encontrar tu clave API en Configuraci\u00f3n > General en la IU web de Radarr." + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Se va a eliminar la configuraci\u00f3n de Radarr mediante YAML. \n\nTu configuraci\u00f3n YAML existente se ha importado a la IU autom\u00e1ticamente. \n\nElimina la configuraci\u00f3n YAML de Radarr de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "title": "Se va a eliminar la configuraci\u00f3n YAML de Radarr" + }, + "removed_attributes": { + "description": "Se han realizado algunos cambios importantes al deshabilitar el sensor de conteo de pel\u00edculas por precauci\u00f3n. \n\nEste sensor puede causar problemas con bases de datos enormes. Si a\u00fan deseas utilizarlo, puedes hacerlo. \n\nLos nombres de las pel\u00edculas ya no se incluyen como atributos en el sensor de pel\u00edculas. \n\nPr\u00f3ximamente ha sido eliminado. Se est\u00e1 modernizando como deber\u00edan ser los elementos del calendario. El espacio en disco ahora se divide en diferentes sensores, uno para cada carpeta. \n\nEl estado y los comandos se han eliminado porque no parecen tener un valor real para las automatizaciones.", + "title": "Cambios en la integraci\u00f3n de Radarr" + } + }, + "options": { + "step": { + "init": { + "data": { + "upcoming_days": "N\u00famero de d\u00edas pr\u00f3ximos a mostrar" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/radarr/translations/fr.json b/homeassistant/components/radarr/translations/fr.json new file mode 100644 index 00000000000..5f8d2a9f78d --- /dev/null +++ b/homeassistant/components/radarr/translations/fr.json @@ -0,0 +1,45 @@ +{ + "config": { + "abort": { + "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", + "invalid_auth": "Authentification non valide", + "unknown": "Erreur inattendue", + "wrong_app": "Une application incorrecte a \u00e9t\u00e9 atteinte. Veuillez r\u00e9essayer", + "zeroconf_failed": "La cl\u00e9 d'API n'a pas \u00e9t\u00e9 trouv\u00e9e. Veuillez la saisir manuellement" + }, + "step": { + "reauth_confirm": { + "description": "L'int\u00e9gration Radarr doit \u00eatre r\u00e9-authentifi\u00e9e manuellement avec l'API Radarr", + "title": "R\u00e9-authentifier l'int\u00e9gration" + }, + "user": { + "data": { + "api_key": "Cl\u00e9 d'API", + "url": "URL", + "verify_ssl": "V\u00e9rifier le certificat SSL" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "title": "La configuration YAML pour Radarr sera bient\u00f4t supprim\u00e9e" + }, + "removed_attributes": { + "title": "Modifications apport\u00e9es \u00e0 l'int\u00e9gration Radarr" + } + }, + "options": { + "step": { + "init": { + "data": { + "upcoming_days": "Nombre de jours \u00e0 venir \u00e0 afficher" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/radarr/translations/he.json b/homeassistant/components/radarr/translations/he.json new file mode 100644 index 00000000000..44273d609c2 --- /dev/null +++ b/homeassistant/components/radarr/translations/he.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "\u05e9\u05d9\u05e8\u05d5\u05ea \u05d6\u05d4 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8", + "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "step": { + "reauth_confirm": { + "title": "\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05e9\u05dc \u05e9\u05d9\u05dc\u05d5\u05d1" + }, + "user": { + "data": { + "api_key": "\u05de\u05e4\u05ea\u05d7 API", + "url": "\u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8", + "verify_ssl": "\u05d0\u05d9\u05de\u05d5\u05ea \u05d0\u05d9\u05e9\u05d5\u05e8 SSL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/radarr/translations/hu.json b/homeassistant/components/radarr/translations/hu.json new file mode 100644 index 00000000000..f00034f5f5f --- /dev/null +++ b/homeassistant/components/radarr/translations/hu.json @@ -0,0 +1,48 @@ +{ + "config": { + "abort": { + "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", + "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", + "zeroconf_failed": "Az API-kulcs nem tal\u00e1lhat\u00f3. K\u00e9rem, adja meg" + }, + "step": { + "reauth_confirm": { + "description": "A Radarr integr\u00e1ci\u00f3j\u00e1t manu\u00e1lisan kell \u00fajra hiteles\u00edteni a Radarr API-val", + "title": "Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se" + }, + "user": { + "data": { + "api_key": "API kulcs", + "url": "URL", + "verify_ssl": "SSL-tan\u00fas\u00edtv\u00e1ny ellen\u0151rz\u00e9se" + }, + "description": "Az API-kulcs automatikusan lek\u00e9rhet\u0151, ha a bejelentkez\u00e9si hiteles\u00edt\u0151 adatok nem lettek be\u00e1ll\u00edtva az alkalmaz\u00e1sban.\nAz API-kulcs a Radarr webes felhaszn\u00e1l\u00f3i fel\u00fclet Be\u00e1ll\u00edt\u00e1sok > \u00c1ltal\u00e1nos men\u00fcpontj\u00e1ban tal\u00e1lhat\u00f3." + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "A Radarr YAML haszn\u00e1lat\u00e1val t\u00f6rt\u00e9n\u0151 konfigur\u00e1l\u00e1sa elt\u00e1vol\u00edt\u00e1sra ker\u00fcl.\n\nA megl\u00e9v\u0151 YAML konfigur\u00e1ci\u00f3 automatikusan import\u00e1l\u00e1sra ker\u00fclt a felhaszn\u00e1l\u00f3i fel\u00fcletre.\n\nA hiba kijav\u00edt\u00e1s\u00e1hoz t\u00e1vol\u00edtsa el a Radarr YAML konfigur\u00e1ci\u00f3t a configuration.yaml f\u00e1jlb\u00f3l, \u00e9s ind\u00edtsa \u00fajra a Home Assistantot.", + "title": "A Radarr YAML konfigur\u00e1ci\u00f3 elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" + }, + "removed_attributes": { + "description": "N\u00e9h\u00e1ny v\u00e1ltoztat\u00e1s t\u00f6rt\u00e9nt a Filmek sz\u00e1m\u00e1nak \u00e9rz\u00e9kel\u0151j\u00e9nek \u00f3vatoss\u00e1gb\u00f3l t\u00f6rt\u00e9n\u0151 letilt\u00e1s\u00e1ban.\n\nEz az \u00e9rz\u00e9kel\u0151 probl\u00e9m\u00e1kat okozhat hatalmas adatb\u00e1zisok eset\u00e9n. Ha tov\u00e1bbra is haszn\u00e1lni szeretn\u00e9, megteheti.\n\nA filmek nevei t\u00f6bb\u00e9 nem szerepelnek attrib\u00fatumk\u00e9nt a filmek \u00e9rz\u00e9kel\u0151ben.\n\nAz Upcoming elt\u00e1vol\u00edt\u00e1sra ker\u00fclt. Korszer\u0171s\u00edt\u00e9sre ker\u00fcl, ahogyan a napt\u00e1relemeknek is kell. A lemezter\u00fclet mostant\u00f3l k\u00fcl\u00f6nb\u00f6z\u0151 \u00e9rz\u00e9kel\u0151kre van felosztva, egy-egy mapp\u00e1hoz.\n\nA st\u00e1tusz \u00e9s a parancsok elt\u00e1vol\u00edt\u00e1sra ker\u00fcltek, mivel \u00fagy t\u0171nik, hogy nincs val\u00f3di \u00e9rt\u00e9k\u00fck az automatiz\u00e1l\u00e1sok sz\u00e1m\u00e1ra.", + "title": "A Radarr-integr\u00e1ci\u00f3 v\u00e1ltoz\u00e1sai" + } + }, + "options": { + "step": { + "init": { + "data": { + "upcoming_days": "A megjelen\u00edteni k\u00edv\u00e1nt k\u00f6vetkez\u0151 napok sz\u00e1ma" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/radarr/translations/id.json b/homeassistant/components/radarr/translations/id.json new file mode 100644 index 00000000000..f6ad3195c71 --- /dev/null +++ b/homeassistant/components/radarr/translations/id.json @@ -0,0 +1,48 @@ +{ + "config": { + "abort": { + "already_configured": "Layanan sudah dikonfigurasi", + "reauth_successful": "Autentikasi ulang berhasil" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid", + "unknown": "Kesalahan yang tidak diharapkan", + "wrong_app": "Aplikasi yang salah tercapai. Silakan coba lagi", + "zeroconf_failed": "Kunci API tidak ditemukan. Silakan masukkan secara manual" + }, + "step": { + "reauth_confirm": { + "description": "Integrasi Radarr perlu diautentikasi ulang secara manual dengan Radarr API", + "title": "Autentikasi Ulang Integrasi" + }, + "user": { + "data": { + "api_key": "Kunci API", + "url": "URL", + "verify_ssl": "Verifikasi sertifikat SSL" + }, + "description": "Kunci API dapat diambil secara otomatis jika kredensial login tidak diatur dalam aplikasi.\nKunci API Anda dapat ditemukan di Settings > General di antarmuka web Radarr." + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Proses konfigurasi Radarr lewat YAML dalam proses penghapusan.\n\nKonfigurasi YAML yang ada telah diimpor ke antarmuka secara otomatis.\n\nHapus konfigurasi YAML Radarr dari file configuration.yaml dan mulai ulang Home Assistant untuk memperbaiki masalah ini.", + "title": "Konfigurasi YAML Radarr dalam proses penghapusan" + }, + "removed_attributes": { + "description": "Beberapa perubahan besar telah dilakukan dalam menonaktifkan sensor hitungan Film dengan alasan kehati-hatian.\n\nSensor ini bisa menyebabkan masalah dengan database yang sangat besar. Jika masih ingin menggunakannya, Anda dapat melakukannya.\n\nNama film tidak lagi disertakan sebagai atribut dalam sensor film.\n\nItem \"Yang akan datang\" telah dihapus. Sensor ini sedang dimodernisasi sebagaimana layaknya item kalender. Ruang disk sekarang dipecah ke dalam sensor yang berbeda, satu untuk setiap folder.\n\nStatus dan perintah telah dihapus karena tampaknya tidak membawa nilai dalam otomasi.", + "title": "Perubahan pada integrasi Radarr" + } + }, + "options": { + "step": { + "init": { + "data": { + "upcoming_days": "Jumlah hari mendatang untuk ditampilkan" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/radarr/translations/it.json b/homeassistant/components/radarr/translations/it.json new file mode 100644 index 00000000000..afc64b6bc8e --- /dev/null +++ b/homeassistant/components/radarr/translations/it.json @@ -0,0 +1,48 @@ +{ + "config": { + "abort": { + "already_configured": "Il servizio \u00e8 gi\u00e0 configurato", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida", + "unknown": "Errore imprevisto", + "wrong_app": "Applicazione errata raggiunta. Per favore riprova", + "zeroconf_failed": "Chiave API non trovata. Si prega di inserirla manualmente" + }, + "step": { + "reauth_confirm": { + "description": "L'integrazione Radarr deve essere di nuovo autenticata manualmente con l'API Radarr.", + "title": "Autentica nuovamente l'integrazione" + }, + "user": { + "data": { + "api_key": "Chiave API", + "url": "URL", + "verify_ssl": "Verifica il certificato SSL" + }, + "description": "La chiave API pu\u00f2 essere recuperata automaticamente se le credenziali di accesso non sono state impostate nell'applicazione.\nLa chiave API pu\u00f2 essere trovata in Impostazioni > Generali nell'interfaccia Web di Radarr." + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "La configurazione di Radarr tramite YAML \u00e8 stata rimossa.\n\nLa configurazione YAML esistente \u00e8 stata importata automaticamente nell'interfaccia utente.\n\nRimuovere la configurazione YAML di Radarr dal file configuration.yaml e riavviare Home Assistant per risolvere il problema.", + "title": "La configurazione YAML di Radarr \u00e8 stata rimossa" + }, + "removed_attributes": { + "description": "Sono state apportate alcune modifiche alla disabilitazione del sensore di conteggio dei filmati per prudenza.\n\nQuesto sensore pu\u00f2 causare problemi con database di grandi dimensioni. Se si desidera ancora utilizzarlo, \u00e8 possibile farlo.\n\nI nomi dei film non sono pi\u00f9 inclusi come attributi nel sensore dei film.\n\nLa voce Upcoming \u00e8 stata rimossa. \u00c8 stato modernizzato come dovrebbero essere gli elementi del calendario. Lo spazio su disco \u00e8 ora suddiviso in diversi sensori, uno per ogni cartella.\n\nStato e comandi sono stati rimossi perch\u00e9 non sembrano avere un valore reale per le automazioni.", + "title": "Modifiche all'integrazione Radarr" + } + }, + "options": { + "step": { + "init": { + "data": { + "upcoming_days": "Numero dei prossimi giorni da visualizzare" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/radarr/translations/nl.json b/homeassistant/components/radarr/translations/nl.json new file mode 100644 index 00000000000..436d0998a9e --- /dev/null +++ b/homeassistant/components/radarr/translations/nl.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Dienst is al geconfigureerd", + "reauth_successful": "Herauthenticatie geslaagd" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "invalid_auth": "Ongeldige authenticatie", + "unknown": "Onverwachte fout" + }, + "step": { + "reauth_confirm": { + "title": "Integratie herauthenticeren" + }, + "user": { + "data": { + "api_key": "API-sleutel", + "url": "URL", + "verify_ssl": "SSL-certificaat verifi\u00ebren" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/radarr/translations/no.json b/homeassistant/components/radarr/translations/no.json new file mode 100644 index 00000000000..4b6b2adb523 --- /dev/null +++ b/homeassistant/components/radarr/translations/no.json @@ -0,0 +1,48 @@ +{ + "config": { + "abort": { + "already_configured": "Tjenesten er allerede konfigurert", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning", + "unknown": "Uventet feil", + "wrong_app": "Feil s\u00f8knad er n\u00e5dd. V\u00e6r s\u00e5 snill, pr\u00f8v p\u00e5 nytt", + "zeroconf_failed": "Finner ikke API-n\u00f8kkel. Vennligst skriv det inn manuelt" + }, + "step": { + "reauth_confirm": { + "description": "Radarr-integrasjonen m\u00e5 re-autentiseres manuelt med Radarr API", + "title": "Godkjenne integrering p\u00e5 nytt" + }, + "user": { + "data": { + "api_key": "API-n\u00f8kkel", + "url": "URL", + "verify_ssl": "Verifisere SSL-sertifikat" + }, + "description": "API-n\u00f8kkel kan hentes automatisk hvis p\u00e5loggingsinformasjon ikke ble angitt i applikasjonen.\n API-n\u00f8kkelen din finner du i Innstillinger > Generelt i Radarr Web UI." + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Konfigurering av Radarr med YAML blir fjernet. \n\n Din eksisterende YAML-konfigurasjon har blitt importert til brukergrensesnittet automatisk. \n\n Fjern Radarr YAML-konfigurasjonen fra configuration.yaml-filen og start Home Assistant p\u00e5 nytt for \u00e5 fikse dette problemet.", + "title": "Radarr YAML-konfigurasjonen blir fjernet" + }, + "removed_attributes": { + "description": "Noen bruddendringer er gjort for \u00e5 deaktivere filmtellingssensoren ut av forsiktighet. \n\n Denne sensoren kan for\u00e5rsake problemer med massive databaser. Hvis du fortsatt \u00f8nsker \u00e5 bruke den, kan du gj\u00f8re det. \n\n Filmnavn er ikke lenger inkludert som attributter i filmsensoren. \n\n Kommende er fjernet. Den moderniseres slik kalenderposter skal v\u00e6re. Diskplass er n\u00e5 delt inn i forskjellige sensorer, en for hver mappe. \n\n Status og kommandoer er fjernet da de ikke ser ut til \u00e5 ha reell verdi for automatisering.", + "title": "Endringer i Radarr-integrasjonen" + } + }, + "options": { + "step": { + "init": { + "data": { + "upcoming_days": "Antall kommende dager \u00e5 vise" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/radarr/translations/pt-BR.json b/homeassistant/components/radarr/translations/pt-BR.json new file mode 100644 index 00000000000..74d33fa6136 --- /dev/null +++ b/homeassistant/components/radarr/translations/pt-BR.json @@ -0,0 +1,48 @@ +{ + "config": { + "abort": { + "already_configured": "O servi\u00e7o 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", + "unknown": "Erro inesperado", + "wrong_app": "Aplica\u00e7\u00e3o incorreta alcan\u00e7ada. Por favor, tente novamente", + "zeroconf_failed": "Chave de API n\u00e3o encontrada. Por favor, insira-o manualmente" + }, + "step": { + "reauth_confirm": { + "description": "A integra\u00e7\u00e3o do Radarr precisa ser autenticada manualmente com a API do Radarr", + "title": "Reautenticar Integra\u00e7\u00e3o" + }, + "user": { + "data": { + "api_key": "Chave de API", + "url": "URL", + "verify_ssl": "Verificar certificado SSL" + }, + "description": "A chave de API pode ser recuperada automaticamente se as credenciais de login n\u00e3o tiverem sido definidas no aplicativo.\n Sua chave de API pode ser encontrada em Configura\u00e7\u00f5es > Geral na interface da Web do Radarr." + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "A configura\u00e7\u00e3o do Radarr usando YAML est\u00e1 sendo removida. \n\n Sua configura\u00e7\u00e3o YAML existente foi importada para a interface do usu\u00e1rio automaticamente. \n\n Remova a configura\u00e7\u00e3o YAML do Radarr do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "A configura\u00e7\u00e3o YAML do Radarr est\u00e1 sendo removida" + }, + "removed_attributes": { + "description": "Algumas mudan\u00e7as importantes foram feitas na desativa\u00e7\u00e3o do sensor de contagem de filmes por precau\u00e7\u00e3o. \n\n Este sensor pode causar problemas com bancos de dados massivos. Se voc\u00ea ainda deseja us\u00e1-lo, voc\u00ea pode faz\u00ea-lo. \n\n Os nomes dos filmes n\u00e3o s\u00e3o mais inclu\u00eddos como atributos no sensor de filmes. \n\n O pr\u00f3ximo foi removido. Ele est\u00e1 sendo modernizado como os itens do calend\u00e1rio devem ser. O espa\u00e7o em disco agora \u00e9 dividido em diferentes sensores, um para cada pasta. \n\n Status e comandos foram removidos, pois n\u00e3o parecem ter valor real para automa\u00e7\u00f5es.", + "title": "Mudan\u00e7as na integra\u00e7\u00e3o do Radarr" + } + }, + "options": { + "step": { + "init": { + "data": { + "upcoming_days": "N\u00famero de pr\u00f3ximos dias a serem exibidos" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/radarr/translations/ru.json b/homeassistant/components/radarr/translations/ru.json new file mode 100644 index 00000000000..46fd877aa3a --- /dev/null +++ b/homeassistant/components/radarr/translations/ru.json @@ -0,0 +1,48 @@ +{ + "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": { + "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.", + "wrong_app": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435. \u041f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0451 \u0440\u0430\u0437.", + "zeroconf_failed": "\u041a\u043b\u044e\u0447 API \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u0435\u0433\u043e \u0432\u0440\u0443\u0447\u043d\u0443\u044e." + }, + "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\u0438 \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e API Radarr", + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" + }, + "user": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API", + "url": "URL-\u0430\u0434\u0440\u0435\u0441", + "verify_ssl": "\u041f\u0440\u043e\u0432\u0435\u0440\u044f\u0442\u044c \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL" + }, + "description": "\u041a\u043b\u044e\u0447 API \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u043f\u043e\u043b\u0443\u0447\u0435\u043d \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438, \u0435\u0441\u043b\u0438 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0434\u043b\u044f \u0432\u0445\u043e\u0434\u0430 \u043d\u0435 \u0431\u044b\u043b\u0438 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u044b \u0432 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0438.\n\u0412\u0430\u0448 \u043a\u043b\u044e\u0447 API \u043c\u043e\u0436\u043d\u043e \u043d\u0430\u0439\u0442\u0438 \u0432 \u0440\u0430\u0437\u0434\u0435\u043b\u0435 \u00ab\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 > \u00ab\u041e\u0441\u043d\u043e\u0432\u043d\u044b\u0435\u00bb \u0432 \u0432\u0435\u0431-\u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0435 Radarr." + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Radarr \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f YAML-\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0430. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0435\u0451 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "title": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Radarr \u0447\u0435\u0440\u0435\u0437 YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430" + }, + "removed_attributes": { + "description": "\u0412 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u043c\u0435\u0440\u044b \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u043e\u0440\u043e\u0436\u043d\u043e\u0441\u0442\u0438 \u0431\u044b\u043b \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d \u0441\u0435\u043d\u0441\u043e\u0440 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u0430 \u0444\u0438\u043b\u044c\u043c\u043e\u0432. \u042d\u0442\u043e\u0442 \u0441\u0435\u043d\u0441\u043e\u0440 \u043c\u043e\u0436\u0435\u0442 \u0432\u044b\u0437\u0432\u0430\u0442\u044c \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u044b \u0441 \u043c\u0430\u0441\u0441\u0438\u0432\u043d\u044b\u043c\u0438 \u0431\u0430\u0437\u0430\u043c\u0438 \u0434\u0430\u043d\u043d\u044b\u0445, \u043d\u043e \u043f\u0440\u0438 \u0436\u0435\u043b\u0430\u043d\u0438\u0438 \u0412\u044b \u0432\u0441\u0435 \u0435\u0449\u0451 \u043c\u043e\u0436\u0435\u0442\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0435\u0433\u043e.\n\n\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u044f \u0444\u0438\u043b\u044c\u043c\u043e\u0432 \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435 \u0432\u043a\u043b\u044e\u0447\u0430\u044e\u0442\u0441\u044f \u0432 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u0430\u0442\u0440\u0438\u0431\u0443\u0442\u043e\u0432 \u0432 \u0441\u0435\u043d\u0441\u043e\u0440 \u0444\u0438\u043b\u044c\u043c\u043e\u0432.\n\n\u041f\u0440\u0435\u0434\u0441\u0442\u043e\u044f\u0449\u0435\u0435 \u0431\u044b\u043b\u043e \u0443\u0434\u0430\u043b\u0435\u043d\u043e. \u041e\u043d\u043e \u043c\u043e\u0434\u0435\u0440\u043d\u0438\u0437\u0438\u0440\u043e\u0432\u0430\u043d\u043e, \u043a\u0430\u043a \u0438 \u0434\u0440\u0443\u0433\u0438\u0435 \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u044b \u043a\u0430\u043b\u0435\u043d\u0434\u0430\u0440\u044f. \u0414\u0438\u0441\u043a\u043e\u0432\u043e\u0435 \u043f\u0440\u043e\u0441\u0442\u0440\u0430\u043d\u0441\u0442\u0432\u043e \u0442\u0435\u043f\u0435\u0440\u044c \u0440\u0430\u0437\u0434\u0435\u043b\u0435\u043d\u043e \u043d\u0430 \u0440\u0430\u0437\u043d\u044b\u0435 \u0441\u0435\u043d\u0441\u043e\u0440\u044b, \u043f\u043e \u043e\u0434\u043d\u043e\u043c\u0443 \u0434\u043b\u044f \u043a\u0430\u0436\u0434\u043e\u0439 \u043f\u0430\u043f\u043a\u0438.\n\n\u0421\u0442\u0430\u0442\u0443\u0441 \u0438 \u043a\u043e\u043c\u0430\u043d\u0434\u044b \u0431\u044b\u043b\u0438 \u0443\u0434\u0430\u043b\u0435\u043d\u044b, \u0442\u0430\u043a \u043a\u0430\u043a \u043e\u043d\u0438 \u043d\u0435 \u0438\u043c\u0435\u044e\u0442 \u0440\u0435\u0430\u043b\u044c\u043d\u043e\u0439 \u0446\u0435\u043d\u043d\u043e\u0441\u0442\u0438 \u0434\u043b\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0438\u0438.", + "title": "\u0418\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u044f \u0432 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 Radarr" + } + }, + "options": { + "step": { + "init": { + "data": { + "upcoming_days": "\u041a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u043f\u0440\u0435\u0434\u0441\u0442\u043e\u044f\u0449\u0438\u0445 \u0434\u043d\u0435\u0439 \u0434\u043b\u044f \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/radarr/translations/zh-Hant.json b/homeassistant/components/radarr/translations/zh-Hant.json new file mode 100644 index 00000000000..6b381643c1a --- /dev/null +++ b/homeassistant/components/radarr/translations/zh-Hant.json @@ -0,0 +1,48 @@ +{ + "config": { + "abort": { + "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", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4", + "wrong_app": "\u5b58\u53d6\u61c9\u7528\u7a0b\u5f0f\u4e0d\u6b63\u78ba\uff0c\u8acb\u518d\u8a66\u4e00\u6b21", + "zeroconf_failed": "\u627e\u4e0d\u5230 API \u91d1\u9470\u3001\u8acb\u624b\u52d5\u8f38\u5165\u3002" + }, + "step": { + "reauth_confirm": { + "description": "Radarr \u6574\u5408\u9700\u8981\u624b\u52d5\u91cd\u65b0\u8a8d\u8b49 Radarr API", + "title": "\u91cd\u65b0\u8a8d\u8b49\u6574\u5408" + }, + "user": { + "data": { + "api_key": "API \u91d1\u9470", + "url": "\u7db2\u5740", + "verify_ssl": "\u78ba\u8a8d SSL \u8a8d\u8b49" + }, + "description": "\u5047\u5982\u6c92\u6709\u65bc\u61c9\u7528\u7a0b\u5f0f\u4e2d\u8a2d\u5b9a\u767b\u5165\u6191\u8b49\uff0c\u5247\u53ef\u4ee5\u81ea\u52d5\u53d6\u5f97 API \u91d1\u9470\u3002\n\u91d1\u9470\u53ef\u4ee5\u65bc Radarr Web \u4ecb\u9762\u4e2d\u8a2d\u5b9a\uff08Settings\uff09 > \u4e00\u822c\uff08General\uff09\u4e2d\u53d6\u5f97\u3002" + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a\u7684 Radarr \u5373\u5c07\u9032\u884c\u79fb\u9664\u3002\n\n\u65e2\u6709\u7684 YAML \u8a2d\u5b9a\u5c07\u81ea\u52d5\u532f\u5165\u81f3 UI \u5167\u3002\n\n\u8acb\u65bc configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 Radarr YAML \u8a2d\u5b9a\u4e26\u91cd\u65b0\u555f\u52d5 Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "Radarr YAML \u8a2d\u5b9a\u5373\u5c07\u79fb\u9664" + }, + "removed_attributes": { + "description": "\u5728\u8b39\u614e\u8003\u91cf\u5f8c\u3001\u95dc\u9589\u96fb\u5f71\u6578\u611f\u6e2c\u5668\u4e0a\u505a\u4e86\u4e00\u4e9b\u91cd\u5927\u8b8a\u66f4\u3002\n\n\u7531\u65bc\u5de8\u5927\u7684\u8cc7\u6599\u5eab\u53ef\u80fd\u6703\u5c0e\u81f4\u611f\u6e2c\u5668\u51fa\u73fe\u554f\u984c\u3002\u5047\u5982\u60a8\u4ecd\u60f3\u7e7c\u7e8c\u4f7f\u7528\u3001\u8acb\u6ce8\u610f\u76f8\u95dc\u554f\u984c\u3002\n\n\u96fb\u5f71\u611f\u6e2c\u5668\u5c6c\u6027\u4e2d\u5c07\u4e0d\u518d\u5305\u542b\u96fb\u5f71\u540d\u7a31\u3002\n\n\u5373\u5c07\u4e0a\u6620\u90e8\u5206\u5df2\u7d93\u79fb\u9664\u3002\u6b63\u8ddf\u8457\u884c\u4e8b\u66c6\u529f\u80fd\u9032\u884c\u66f4\u65b0\uff0c\u78c1\u789f\u7a7a\u9593\u5c07\u6703\u4f9d\u64da\u4e0d\u540c\u611f\u6e2c\u5668\u9032\u884c\u5206\u9694\u65bc\u5404\u81ea\u7684\u8cc7\u6599\u593e\u3002\n\n\u7531\u65bc\u5c0d\u65bc\u81ea\u52d5\u5316\u7684\u7528\u9014\u4e0d\u9ad8\uff0c\u72c0\u614b\u8207\u547d\u4ee4\u4e5f\u5df2\u7d93\u79fb\u9664\u3002", + "title": "\u8b8a\u66f4\u81f3 Radarr \u6574\u5408" + } + }, + "options": { + "step": { + "init": { + "data": { + "upcoming_days": "\u5373\u5c07\u5230\u4f86\u986f\u793a\u5929\u6578" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/radio_browser/media_source.py b/homeassistant/components/radio_browser/media_source.py index 6ba1b7b2b9a..e49f670d371 100644 --- a/homeassistant/components/radio_browser/media_source.py +++ b/homeassistant/components/radio_browser/media_source.py @@ -5,13 +5,7 @@ import mimetypes from radios import FilterBy, Order, RadioBrowser, Station -from homeassistant.components.media_player.const import ( - MEDIA_CLASS_CHANNEL, - MEDIA_CLASS_DIRECTORY, - MEDIA_CLASS_MUSIC, - MEDIA_TYPE_MUSIC, -) -from homeassistant.components.media_player.errors import BrowseError +from homeassistant.components.media_player import BrowseError, MediaClass, MediaType from homeassistant.components.media_source.error import Unresolvable from homeassistant.components.media_source.models import ( BrowseMediaSource, @@ -88,12 +82,12 @@ class RadioMediaSource(MediaSource): return BrowseMediaSource( domain=DOMAIN, identifier=None, - media_class=MEDIA_CLASS_CHANNEL, - media_content_type=MEDIA_TYPE_MUSIC, + media_class=MediaClass.CHANNEL, + media_content_type=MediaType.MUSIC, title=self.entry.title, can_play=False, can_expand=True, - children_media_class=MEDIA_CLASS_DIRECTORY, + children_media_class=MediaClass.DIRECTORY, children=[ *await self._async_build_popular(radios, item), *await self._async_build_by_tag(radios, item), @@ -128,7 +122,7 @@ class RadioMediaSource(MediaSource): BrowseMediaSource( domain=DOMAIN, identifier=station.uuid, - media_class=MEDIA_CLASS_MUSIC, + media_class=MediaClass.MUSIC, media_content_type=mime_type, title=station.name, can_play=True, @@ -161,8 +155,8 @@ class RadioMediaSource(MediaSource): BrowseMediaSource( domain=DOMAIN, identifier=f"country/{country.code}", - media_class=MEDIA_CLASS_DIRECTORY, - media_content_type=MEDIA_TYPE_MUSIC, + media_class=MediaClass.DIRECTORY, + media_content_type=MediaType.MUSIC, title=country.name, can_play=False, can_expand=True, @@ -194,8 +188,8 @@ class RadioMediaSource(MediaSource): BrowseMediaSource( domain=DOMAIN, identifier=f"language/{language.code}", - media_class=MEDIA_CLASS_DIRECTORY, - media_content_type=MEDIA_TYPE_MUSIC, + media_class=MediaClass.DIRECTORY, + media_content_type=MediaType.MUSIC, title=language.name, can_play=False, can_expand=True, @@ -209,8 +203,8 @@ class RadioMediaSource(MediaSource): BrowseMediaSource( domain=DOMAIN, identifier="language", - media_class=MEDIA_CLASS_DIRECTORY, - media_content_type=MEDIA_TYPE_MUSIC, + media_class=MediaClass.DIRECTORY, + media_content_type=MediaType.MUSIC, title="By Language", can_play=False, can_expand=True, @@ -237,8 +231,8 @@ class RadioMediaSource(MediaSource): BrowseMediaSource( domain=DOMAIN, identifier="popular", - media_class=MEDIA_CLASS_DIRECTORY, - media_content_type=MEDIA_TYPE_MUSIC, + media_class=MediaClass.DIRECTORY, + media_content_type=MediaType.MUSIC, title="Popular", can_play=False, can_expand=True, @@ -277,8 +271,8 @@ class RadioMediaSource(MediaSource): BrowseMediaSource( domain=DOMAIN, identifier=f"tag/{tag.name}", - media_class=MEDIA_CLASS_DIRECTORY, - media_content_type=MEDIA_TYPE_MUSIC, + media_class=MediaClass.DIRECTORY, + media_content_type=MediaType.MUSIC, title=tag.name.title(), can_play=False, can_expand=True, @@ -291,8 +285,8 @@ class RadioMediaSource(MediaSource): BrowseMediaSource( domain=DOMAIN, identifier="tag", - media_class=MEDIA_CLASS_DIRECTORY, - media_content_type=MEDIA_TYPE_MUSIC, + media_class=MediaClass.DIRECTORY, + media_content_type=MediaType.MUSIC, title="By Category", can_play=False, can_expand=True, diff --git a/homeassistant/components/radiotherm/climate.py b/homeassistant/components/radiotherm/climate.py index cd2bb69ad58..f8ccf068f69 100644 --- a/homeassistant/components/radiotherm/climate.py +++ b/homeassistant/components/radiotherm/climate.py @@ -7,13 +7,14 @@ from typing import Any import radiotherm import voluptuous as vol -from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( FAN_AUTO, FAN_OFF, FAN_ON, + PLATFORM_SCHEMA, PRESET_AWAY, PRESET_HOME, + ClimateEntity, ClimateEntityFeature, HVACAction, HVACMode, diff --git a/homeassistant/components/radiotherm/translations/cs.json b/homeassistant/components/radiotherm/translations/cs.json new file mode 100644 index 00000000000..bfa5884e24c --- /dev/null +++ b/homeassistant/components/radiotherm/translations/cs.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "confirm": { + "description": "Chcete nastavit {name} {model} ({host})?" + }, + "user": { + "data": { + "host": "Hostitel" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/radiotherm/translations/fr.json b/homeassistant/components/radiotherm/translations/fr.json index 47705ce5138..ed56ecb8ac6 100644 --- a/homeassistant/components/radiotherm/translations/fr.json +++ b/homeassistant/components/radiotherm/translations/fr.json @@ -19,6 +19,11 @@ } } }, + "issues": { + "deprecated_yaml": { + "title": "La configuration YAML pour Radio\u00a0Thermostat sera bient\u00f4t supprim\u00e9e" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/raincloud/binary_sensor.py b/homeassistant/components/raincloud/binary_sensor.py index 888529b5c38..b3766cd02cd 100644 --- a/homeassistant/components/raincloud/binary_sensor.py +++ b/homeassistant/components/raincloud/binary_sensor.py @@ -58,7 +58,7 @@ class RainCloudBinarySensor(RainCloudEntity, BinarySensorEntity): """Return true if the binary sensor is on.""" return self._state - def update(self): + def update(self) -> None: """Get the latest data and updates the state.""" _LOGGER.debug("Updating RainCloud sensor: %s", self._name) self._state = getattr(self.data, self._sensor_type) diff --git a/homeassistant/components/raincloud/sensor.py b/homeassistant/components/raincloud/sensor.py index b07ccd1e7ac..4d21d36d069 100644 --- a/homeassistant/components/raincloud/sensor.py +++ b/homeassistant/components/raincloud/sensor.py @@ -66,7 +66,7 @@ class RainCloudSensor(RainCloudEntity, SensorEntity): """Return the units of measurement.""" return UNIT_OF_MEASUREMENT_MAP.get(self._sensor_type) - def update(self): + def update(self) -> None: """Get the latest data and updates the states.""" _LOGGER.debug("Updating RainCloud sensor: %s", self._name) if self._sensor_type == "battery": diff --git a/homeassistant/components/raincloud/switch.py b/homeassistant/components/raincloud/switch.py index b783a3d9375..89b2673f66e 100644 --- a/homeassistant/components/raincloud/switch.py +++ b/homeassistant/components/raincloud/switch.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any import voluptuous as vol @@ -68,7 +69,7 @@ class RainCloudSwitch(RainCloudEntity, SwitchEntity): """Return true if device is on.""" return self._state - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" if self._sensor_type == "manual_watering": self.data.watering_time = self._default_watering_timer @@ -76,7 +77,7 @@ class RainCloudSwitch(RainCloudEntity, SwitchEntity): self.data.auto_watering = True self._state = True - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" if self._sensor_type == "manual_watering": self.data.watering_time = "off" @@ -84,7 +85,7 @@ class RainCloudSwitch(RainCloudEntity, SwitchEntity): self.data.auto_watering = False self._state = False - def update(self): + def update(self) -> None: """Update device state.""" _LOGGER.debug("Updating RainCloud switch: %s", self._name) if self._sensor_type == "manual_watering": diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py index 4d19dbc7bfc..8cc3b3d5e80 100644 --- a/homeassistant/components/rainmachine/__init__.py +++ b/homeassistant/components/rainmachine/__init__.py @@ -58,6 +58,7 @@ CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) PLATFORMS = [ Platform.BINARY_SENSOR, Platform.BUTTON, + Platform.SELECT, Platform.SENSOR, Platform.SWITCH, Platform.UPDATE, @@ -496,7 +497,7 @@ class RainMachineEntity(CoordinatorEntity): f"{self._entry.data[CONF_PORT]}" ), connections={(dr.CONNECTION_NETWORK_MAC, self._data.controller.mac)}, - name=str(self._data.controller.name).capitalize(), + name=self._data.controller.name.capitalize(), manufacturer="RainMachine", model=( f"Version {self._version_coordinator.data['hwVer']} " diff --git a/homeassistant/components/rainmachine/binary_sensor.py b/homeassistant/components/rainmachine/binary_sensor.py index 48f11f598c9..212d87f2982 100644 --- a/homeassistant/components/rainmachine/binary_sensor.py +++ b/homeassistant/components/rainmachine/binary_sensor.py @@ -2,6 +2,7 @@ from dataclasses import dataclass from homeassistant.components.binary_sensor import ( + DOMAIN as BINARY_SENSOR_DOMAIN, BinarySensorEntity, BinarySensorEntityDescription, ) @@ -21,7 +22,11 @@ from .model import ( RainMachineEntityDescription, RainMachineEntityDescriptionMixinDataKey, ) -from .util import key_exists +from .util import ( + EntityDomainReplacementStrategy, + async_finish_entity_domain_replacements, + key_exists, +) TYPE_FLOW_SENSOR = "flow_sensor" TYPE_FREEZE = "freeze" @@ -125,6 +130,27 @@ async def async_setup_entry( """Set up RainMachine binary sensors based on a config entry.""" data: RainMachineData = hass.data[DOMAIN][entry.entry_id] + async_finish_entity_domain_replacements( + hass, + entry, + ( + EntityDomainReplacementStrategy( + BINARY_SENSOR_DOMAIN, + f"{data.controller.mac}_freeze_protection", + f"switch.{data.controller.name.lower()}_freeze_protect_enabled", + breaks_in_ha_version="2022.12.0", + remove_old_entity=False, + ), + EntityDomainReplacementStrategy( + BINARY_SENSOR_DOMAIN, + f"{data.controller.mac}_extra_water_on_hot_days", + f"switch.{data.controller.name.lower()}_hot_days_extra_watering", + breaks_in_ha_version="2022.12.0", + remove_old_entity=False, + ), + ), + ) + api_category_sensor_map = { DATA_PROVISION_SETTINGS: ProvisionSettingsBinarySensor, DATA_RESTRICTIONS_CURRENT: CurrentRestrictionsBinarySensor, diff --git a/homeassistant/components/rainmachine/config_flow.py b/homeassistant/components/rainmachine/config_flow.py index c12362591e7..eed80b9c145 100644 --- a/homeassistant/components/rainmachine/config_flow.py +++ b/homeassistant/components/rainmachine/config_flow.py @@ -132,7 +132,7 @@ class RainMachineFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): # access token without using the IP address and password, so we have to # store it: return self.async_create_entry( - title=str(controller.name), + title=controller.name.capitalize(), data={ CONF_IP_ADDRESS: user_input[CONF_IP_ADDRESS], CONF_PASSWORD: user_input[CONF_PASSWORD], diff --git a/homeassistant/components/rainmachine/manifest.json b/homeassistant/components/rainmachine/manifest.json index 6a87110f6f6..ca7543bfb38 100644 --- a/homeassistant/components/rainmachine/manifest.json +++ b/homeassistant/components/rainmachine/manifest.json @@ -3,7 +3,7 @@ "name": "RainMachine", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/rainmachine", - "requirements": ["regenmaschine==2022.09.1"], + "requirements": ["regenmaschine==2022.09.2"], "codeowners": ["@bachya"], "iot_class": "local_polling", "homekit": { diff --git a/homeassistant/components/rainmachine/select.py b/homeassistant/components/rainmachine/select.py new file mode 100644 index 00000000000..0f3b8d10be2 --- /dev/null +++ b/homeassistant/components/rainmachine/select.py @@ -0,0 +1,155 @@ +"""Support for RainMachine selects.""" +from __future__ import annotations + +from dataclasses import dataclass + +from regenmaschine.errors import RainMachineError + +from homeassistant.components.select import SelectEntity, SelectEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_UNIT_SYSTEM_IMPERIAL +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import RainMachineData, RainMachineEntity +from .const import DATA_RESTRICTIONS_UNIVERSAL, DOMAIN +from .model import ( + RainMachineEntityDescription, + RainMachineEntityDescriptionMixinDataKey, +) +from .util import key_exists + + +@dataclass +class RainMachineSelectDescription( + SelectEntityDescription, + RainMachineEntityDescription, + RainMachineEntityDescriptionMixinDataKey, +): + """Describe a generic RainMachine select.""" + + +@dataclass +class FreezeProtectionSelectOption: + """Define an option for a freeze selection select.""" + + api_value: float + imperial_label: str + metric_label: str + + +@dataclass +class FreezeProtectionTemperatureMixin: + """Define an entity description mixin to include an options list.""" + + options: list[FreezeProtectionSelectOption] + + +@dataclass +class FreezeProtectionSelectDescription( + RainMachineSelectDescription, FreezeProtectionTemperatureMixin +): + """Describe a freeze protection temperature select.""" + + +TYPE_FREEZE_PROTECTION_TEMPERATURE = "freeze_protection_temperature" + +SELECT_DESCRIPTIONS = ( + FreezeProtectionSelectDescription( + key=TYPE_FREEZE_PROTECTION_TEMPERATURE, + name="Freeze protection temperature", + icon="mdi:thermometer", + entity_category=EntityCategory.CONFIG, + api_category=DATA_RESTRICTIONS_UNIVERSAL, + data_key="freezeProtectTemp", + options=[ + FreezeProtectionSelectOption( + api_value=0.0, + imperial_label="32°F", + metric_label="0°C", + ), + FreezeProtectionSelectOption( + api_value=2.0, + imperial_label="35.6°F", + metric_label="2°C", + ), + FreezeProtectionSelectOption( + api_value=5.0, + imperial_label="41°F", + metric_label="5°C", + ), + FreezeProtectionSelectOption( + api_value=10.0, + imperial_label="50°F", + metric_label="10°C", + ), + ], + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up RainMachine selects based on a config entry.""" + data: RainMachineData = hass.data[DOMAIN][entry.entry_id] + + entity_map = { + TYPE_FREEZE_PROTECTION_TEMPERATURE: FreezeProtectionTemperatureSelect, + } + + async_add_entities( + entity_map[description.key](entry, data, description, hass.config.units.name) + for description in SELECT_DESCRIPTIONS + if ( + (coordinator := data.coordinators[description.api_category]) is not None + and coordinator.data + and key_exists(coordinator.data, description.data_key) + ) + ) + + +class FreezeProtectionTemperatureSelect(RainMachineEntity, SelectEntity): + """Define a RainMachine select.""" + + entity_description: FreezeProtectionSelectDescription + + def __init__( + self, + entry: ConfigEntry, + data: RainMachineData, + description: FreezeProtectionSelectDescription, + unit_system: str, + ) -> None: + """Initialize.""" + super().__init__(entry, data, description) + + self._api_value_to_label_map = {} + self._label_to_api_value_map = {} + + for option in description.options: + if unit_system == CONF_UNIT_SYSTEM_IMPERIAL: + label = option.imperial_label + else: + label = option.metric_label + self._api_value_to_label_map[option.api_value] = label + self._label_to_api_value_map[label] = option.api_value + + self._attr_options = list(self._label_to_api_value_map) + + async def async_select_option(self, option: str) -> None: + """Change the selected option.""" + try: + await self._data.controller.restrictions.set_universal( + {self.entity_description.data_key: self._label_to_api_value_map[option]} + ) + except RainMachineError as err: + raise HomeAssistantError(f"Error while setting {self.name}: {err}") from err + + @callback + def update_from_latest_data(self) -> None: + """Update the entity when new data is received.""" + raw_value = self.coordinator.data[self.entity_description.data_key] + self._attr_current_option = self._api_value_to_label_map[raw_value] diff --git a/homeassistant/components/rainmachine/sensor.py b/homeassistant/components/rainmachine/sensor.py index 32364e08199..97772c6033a 100644 --- a/homeassistant/components/rainmachine/sensor.py +++ b/homeassistant/components/rainmachine/sensor.py @@ -6,6 +6,7 @@ from datetime import datetime, timedelta from typing import Any, cast from homeassistant.components.sensor import ( + DOMAIN as SENSOR_DOMAIN, RestoreSensor, SensorDeviceClass, SensorEntity, @@ -32,7 +33,13 @@ from .model import ( RainMachineEntityDescriptionMixinDataKey, RainMachineEntityDescriptionMixinUid, ) -from .util import RUN_STATE_MAP, RunStates, key_exists +from .util import ( + RUN_STATE_MAP, + EntityDomainReplacementStrategy, + RunStates, + async_finish_entity_domain_replacements, + key_exists, +) DEFAULT_ZONE_COMPLETION_TIME_WOBBLE_TOLERANCE = timedelta(seconds=5) @@ -127,6 +134,20 @@ async def async_setup_entry( """Set up RainMachine sensors based on a config entry.""" data: RainMachineData = hass.data[DOMAIN][entry.entry_id] + async_finish_entity_domain_replacements( + hass, + entry, + ( + EntityDomainReplacementStrategy( + SENSOR_DOMAIN, + f"{data.controller.mac}_freeze_protect_temp", + f"select.{data.controller.name.lower()}_freeze_protect_temperature", + breaks_in_ha_version="2022.12.0", + remove_old_entity=False, + ), + ), + ) + api_category_sensor_map = { DATA_PROVISION_SETTINGS: ProvisionSettingsSensor, DATA_RESTRICTIONS_UNIVERSAL: UniversalRestrictionsSensor, diff --git a/homeassistant/components/rainmachine/switch.py b/homeassistant/components/rainmachine/switch.py index 029c8c06771..56ac814e2eb 100644 --- a/homeassistant/components/rainmachine/switch.py +++ b/homeassistant/components/rainmachine/switch.py @@ -24,11 +24,16 @@ from . import RainMachineData, RainMachineEntity, async_update_programs_and_zone from .const import ( CONF_ZONE_RUN_TIME, DATA_PROGRAMS, + DATA_RESTRICTIONS_UNIVERSAL, DATA_ZONES, DEFAULT_ZONE_RUN, DOMAIN, ) -from .model import RainMachineEntityDescription, RainMachineEntityDescriptionMixinUid +from .model import ( + RainMachineEntityDescription, + RainMachineEntityDescriptionMixinDataKey, + RainMachineEntityDescriptionMixinUid, +) from .util import RUN_STATE_MAP ATTR_AREA = "area" @@ -130,11 +135,45 @@ def raise_on_request_error( class RainMachineSwitchDescription( SwitchEntityDescription, RainMachineEntityDescription, - RainMachineEntityDescriptionMixinUid, ): """Describe a RainMachine switch.""" +@dataclass +class RainMachineActivitySwitchDescription( + RainMachineSwitchDescription, RainMachineEntityDescriptionMixinUid +): + """Describe a RainMachine activity (program/zone) switch.""" + + +@dataclass +class RainMachineRestrictionSwitchDescription( + RainMachineSwitchDescription, RainMachineEntityDescriptionMixinDataKey +): + """Describe a RainMachine restriction switch.""" + + +TYPE_RESTRICTIONS_FREEZE_PROTECT_ENABLED = "freeze_protect_enabled" +TYPE_RESTRICTIONS_HOT_DAYS_EXTRA_WATERING = "hot_days_extra_watering" + +RESTRICTIONS_SWITCH_DESCRIPTIONS = ( + RainMachineRestrictionSwitchDescription( + key=TYPE_RESTRICTIONS_FREEZE_PROTECT_ENABLED, + name="Freeze protection", + icon="mdi:snowflake-alert", + api_category=DATA_RESTRICTIONS_UNIVERSAL, + data_key="freezeProtectEnabled", + ), + RainMachineRestrictionSwitchDescription( + key=TYPE_RESTRICTIONS_HOT_DAYS_EXTRA_WATERING, + name="Extra water on hot days", + icon="mdi:heat-wave", + api_category=DATA_RESTRICTIONS_UNIVERSAL, + data_key="hotDaysExtraWatering", + ), +) + + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: @@ -158,8 +197,8 @@ async def async_setup_entry( platform.async_register_entity_service(service_name, schema, method) data: RainMachineData = hass.data[DOMAIN][entry.entry_id] + entities: list[RainMachineBaseSwitch] = [] - entities: list[RainMachineActivitySwitch | RainMachineEnabledSwitch] = [] for kind, api_category, switch_class, switch_enabled_class in ( ("program", DATA_PROGRAMS, RainMachineProgram, RainMachineProgramEnabled), ("zone", DATA_ZONES, RainMachineZone, RainMachineZoneEnabled), @@ -173,10 +212,9 @@ async def async_setup_entry( switch_class( entry, data, - RainMachineSwitchDescription( + RainMachineActivitySwitchDescription( key=f"{kind}_{uid}", name=name, - icon="mdi:water", api_category=api_category, uid=uid, ), @@ -188,17 +226,19 @@ async def async_setup_entry( switch_enabled_class( entry, data, - RainMachineSwitchDescription( + RainMachineActivitySwitchDescription( key=f"{kind}_{uid}_enabled", name=f"{name} enabled", - entity_category=EntityCategory.CONFIG, - icon="mdi:cog", api_category=api_category, uid=uid, ), ) ) + # Add switches to control restrictions: + for description in RESTRICTIONS_SWITCH_DESCRIPTIONS: + entities.append(RainMachineRestrictionSwitch(entry, data, description)) + async_add_entities(entities) @@ -246,6 +286,9 @@ class RainMachineBaseSwitch(RainMachineEntity, SwitchEntity): class RainMachineActivitySwitch(RainMachineBaseSwitch): """Define a RainMachine switch to start/stop an activity (program or zone).""" + _attr_icon = "mdi:water" + entity_description: RainMachineActivitySwitchDescription + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the switch off. @@ -284,6 +327,10 @@ class RainMachineActivitySwitch(RainMachineBaseSwitch): class RainMachineEnabledSwitch(RainMachineBaseSwitch): """Define a RainMachine switch to enable/disable an activity (program or zone).""" + _attr_entity_category = EntityCategory.CONFIG + _attr_icon = "mdi:cog" + entity_description: RainMachineActivitySwitchDescription + @callback def update_from_latest_data(self) -> None: """Update the entity when new data is received.""" @@ -360,6 +407,36 @@ class RainMachineProgramEnabled(RainMachineEnabledSwitch): self._update_activities() +class RainMachineRestrictionSwitch(RainMachineBaseSwitch): + """Define a RainMachine restriction setting.""" + + _attr_entity_category = EntityCategory.CONFIG + entity_description: RainMachineRestrictionSwitchDescription + + @raise_on_request_error + async def async_turn_off(self, **kwargs: Any) -> None: + """Disable the restriction.""" + await self._data.controller.restrictions.set_universal( + {self.entity_description.data_key: False} + ) + self._attr_is_on = False + self.async_write_ha_state() + + @raise_on_request_error + async def async_turn_on(self, **kwargs: Any) -> None: + """Enable the restriction.""" + await self._data.controller.restrictions.set_universal( + {self.entity_description.data_key: True} + ) + self._attr_is_on = True + self.async_write_ha_state() + + @callback + def update_from_latest_data(self) -> None: + """Update the entity when new data is received.""" + self._attr_is_on = self.coordinator.data[self.entity_description.data_key] + + class RainMachineZone(RainMachineActivitySwitch): """Define a RainMachine zone.""" diff --git a/homeassistant/components/rainmachine/translations/bg.json b/homeassistant/components/rainmachine/translations/bg.json index 1239915231b..4ba51cf991e 100644 --- a/homeassistant/components/rainmachine/translations/bg.json +++ b/homeassistant/components/rainmachine/translations/bg.json @@ -11,5 +11,17 @@ "title": "\u041f\u043e\u043f\u044a\u043b\u043d\u0435\u0442\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f\u0442\u0430 \u0441\u0438" } } + }, + "issues": { + "replaced_old_entity": { + "fix_flow": { + "step": { + "confirm": { + "title": "\u041e\u0431\u0435\u043a\u0442\u044a\u0442 {old_entity_id} \u0449\u0435 \u0431\u044a\u0434\u0435 \u043f\u0440\u0435\u043c\u0430\u0445\u043d\u0430\u0442" + } + } + }, + "title": "\u041e\u0431\u0435\u043a\u0442\u044a\u0442 {old_entity_id} \u0449\u0435 \u0431\u044a\u0434\u0435 \u043f\u0440\u0435\u043c\u0430\u0445\u043d\u0430\u0442" + } } } \ No newline at end of file diff --git a/homeassistant/components/rainmachine/translations/ca.json b/homeassistant/components/rainmachine/translations/ca.json index d441654ce08..e776a0bcdf3 100644 --- a/homeassistant/components/rainmachine/translations/ca.json +++ b/homeassistant/components/rainmachine/translations/ca.json @@ -18,6 +18,19 @@ } } }, + "issues": { + "replaced_old_entity": { + "fix_flow": { + "step": { + "confirm": { + "description": "Actualitza totes les automatitzacions o 'scripts' que utilitzin aquesta entitat perqu\u00e8 passin a utilitzar l'entitat `{replacement_entity_id}`.", + "title": "L'entitat {old_entity_id} s'eliminar\u00e0" + } + } + }, + "title": "L'entitat {old_entity_id} s'eliminar\u00e0" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/rainmachine/translations/de.json b/homeassistant/components/rainmachine/translations/de.json index 20c49ea30f4..51b6f0814af 100644 --- a/homeassistant/components/rainmachine/translations/de.json +++ b/homeassistant/components/rainmachine/translations/de.json @@ -18,6 +18,19 @@ } } }, + "issues": { + "replaced_old_entity": { + "fix_flow": { + "step": { + "confirm": { + "description": "Aktualisiere alle Automatisierungen oder Skripte, die diese Entit\u00e4t verwenden, um stattdessen `{replacement_entity_id}` zu verwenden.", + "title": "Die Entit\u00e4t {old_entity_id} wird entfernt" + } + } + }, + "title": "Die Entit\u00e4t {old_entity_id} wird entfernt" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/rainmachine/translations/el.json b/homeassistant/components/rainmachine/translations/el.json index a244cc58ab3..313f2bfc1fb 100644 --- a/homeassistant/components/rainmachine/translations/el.json +++ b/homeassistant/components/rainmachine/translations/el.json @@ -18,6 +18,19 @@ } } }, + "issues": { + "replaced_old_entity": { + "fix_flow": { + "step": { + "confirm": { + "description": "\u0395\u03bd\u03b7\u03bc\u03b5\u03c1\u03ce\u03c3\u03c4\u03b5 \u03cc\u03bb\u03bf\u03c5\u03c2 \u03c4\u03bf\u03c5\u03c2 \u03b1\u03c5\u03c4\u03bf\u03bc\u03b1\u03c4\u03b9\u03c3\u03bc\u03bf\u03cd\u03c2 \u03ae \u03c4\u03b1 \u03c3\u03b5\u03bd\u03ac\u03c1\u03b9\u03b1 \u03c0\u03bf\u03c5 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03bf\u03cd\u03bd \u03b1\u03c5\u03c4\u03ae \u03c4\u03b7\u03bd \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1 \u03ce\u03c3\u03c4\u03b5 \u03bd\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03bf\u03cd\u03bd \u03b1\u03bd\u03c4\u03af \u03b3\u03b9\u03b1 \u03b1\u03c5\u03c4\u03ae\u03bd \u03c4\u03bf `{replacement_entity_id}`.", + "title": "\u0397 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1 {old_entity_id} \u03b8\u03b1 \u03b1\u03c6\u03b1\u03b9\u03c1\u03b5\u03b8\u03b5\u03af" + } + } + }, + "title": "\u0397 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1 {old_entity_id} \u03b8\u03b1 \u03b1\u03c6\u03b1\u03b9\u03c1\u03b5\u03b8\u03b5\u03af" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/rainmachine/translations/es.json b/homeassistant/components/rainmachine/translations/es.json index 3e13d925b34..a8bcc7cbd0d 100644 --- a/homeassistant/components/rainmachine/translations/es.json +++ b/homeassistant/components/rainmachine/translations/es.json @@ -18,6 +18,19 @@ } } }, + "issues": { + "replaced_old_entity": { + "fix_flow": { + "step": { + "confirm": { + "description": "Actualiza cualquier automatizaci\u00f3n o script que use esta entidad para usar `{replacement_entity_id}`.", + "title": "Se eliminar\u00e1 la entidad {old_entity_id}" + } + } + }, + "title": "Se eliminar\u00e1 la entidad {old_entity_id}" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/rainmachine/translations/fr.json b/homeassistant/components/rainmachine/translations/fr.json index d9a6c011755..63f5af55527 100644 --- a/homeassistant/components/rainmachine/translations/fr.json +++ b/homeassistant/components/rainmachine/translations/fr.json @@ -18,6 +18,19 @@ } } }, + "issues": { + "replaced_old_entity": { + "fix_flow": { + "step": { + "confirm": { + "description": "Modifiez tout script ou automatisation utilisant cette entit\u00e9 afin qu'ils utilisent `{replacement_entity_id}` \u00e0 la place.", + "title": "L'entit\u00e9 {old_entity_id} sera supprim\u00e9e" + } + } + }, + "title": "L'entit\u00e9 {old_entity_id} sera supprim\u00e9e" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/rainmachine/translations/hu.json b/homeassistant/components/rainmachine/translations/hu.json index 0aa7e0aeeb4..f86c3905108 100644 --- a/homeassistant/components/rainmachine/translations/hu.json +++ b/homeassistant/components/rainmachine/translations/hu.json @@ -18,6 +18,19 @@ } } }, + "issues": { + "replaced_old_entity": { + "fix_flow": { + "step": { + "confirm": { + "description": "Friss\u00edtse az ezt az entit\u00e1st haszn\u00e1l\u00f3 automatiz\u00e1l\u00e1sokat vagy szkripteket, hogy helyette a k\u00f6vetkez\u0151t haszn\u00e1ja: `{replacement_entity_id}`", + "title": "{old_entity_id} entit\u00e1s elt\u00e1vol\u00edt\u00e1sra ker\u00fcl." + } + } + }, + "title": "{old_entity_id} entit\u00e1s elt\u00e1vol\u00edt\u00e1sra ker\u00fcl." + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/rainmachine/translations/id.json b/homeassistant/components/rainmachine/translations/id.json index bcbb9126b2a..8223fb4a792 100644 --- a/homeassistant/components/rainmachine/translations/id.json +++ b/homeassistant/components/rainmachine/translations/id.json @@ -18,6 +18,19 @@ } } }, + "issues": { + "replaced_old_entity": { + "fix_flow": { + "step": { + "confirm": { + "description": "Perbarui semua otomasi atau skrip yang menggunakan entitas ini untuk menggunakan `{replacement_entity_id}`.", + "title": "Entitas {old_entity_id} akan dihapus" + } + } + }, + "title": "Entitas {old_entity_id} akan dihapus" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/rainmachine/translations/it.json b/homeassistant/components/rainmachine/translations/it.json index c63bbd7db11..9cca839ea00 100644 --- a/homeassistant/components/rainmachine/translations/it.json +++ b/homeassistant/components/rainmachine/translations/it.json @@ -18,6 +18,19 @@ } } }, + "issues": { + "replaced_old_entity": { + "fix_flow": { + "step": { + "confirm": { + "description": "Aggiorna tutte le automazioni o gli script che utilizzano questa entit\u00e0 in modo che utilizzino invece `{replacement_entity_id}`.", + "title": "L'entit\u00e0 {old_entity_id} verr\u00e0 rimossa" + } + } + }, + "title": "L'entit\u00e0 {old_entity_id} verr\u00e0 rimossa" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/rainmachine/translations/no.json b/homeassistant/components/rainmachine/translations/no.json index 7fbeda38374..f7e0a758ee0 100644 --- a/homeassistant/components/rainmachine/translations/no.json +++ b/homeassistant/components/rainmachine/translations/no.json @@ -18,6 +18,19 @@ } } }, + "issues": { + "replaced_old_entity": { + "fix_flow": { + "step": { + "confirm": { + "description": "Oppdater eventuelle automatiseringer eller skript som bruker denne enheten til i stedet \u00e5 bruke ` {replacement_entity_id} `.", + "title": "{old_entity_id} vil bli fjernet" + } + } + }, + "title": "{old_entity_id} vil bli fjernet" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/rainmachine/translations/pt-BR.json b/homeassistant/components/rainmachine/translations/pt-BR.json index 6359b1b6ae9..7128423202e 100644 --- a/homeassistant/components/rainmachine/translations/pt-BR.json +++ b/homeassistant/components/rainmachine/translations/pt-BR.json @@ -18,6 +18,19 @@ } } }, + "issues": { + "replaced_old_entity": { + "fix_flow": { + "step": { + "confirm": { + "description": "Atualize quaisquer automa\u00e7\u00f5es ou scripts que usam essa entidade para usar `{replacement_entity_id}`.", + "title": "A entidade {old_entity_id} ser\u00e1 removida" + } + } + }, + "title": "A entidade {old_entity_id} ser\u00e1 removida" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/rainmachine/translations/ru.json b/homeassistant/components/rainmachine/translations/ru.json index 8dbe804ecab..2ed8c6df530 100644 --- a/homeassistant/components/rainmachine/translations/ru.json +++ b/homeassistant/components/rainmachine/translations/ru.json @@ -18,6 +18,19 @@ } } }, + "issues": { + "replaced_old_entity": { + "fix_flow": { + "step": { + "confirm": { + "description": "\u0412 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0438\u044f\u0445 \u0438 \u0441\u043a\u0440\u0438\u043f\u0442\u0430\u0445, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044e\u0449\u0438\u0445 \u044d\u0442\u043e\u0442 \u043e\u0431\u044a\u0435\u043a\u0442, \u0442\u0435\u043f\u0435\u0440\u044c \u0441\u043b\u0435\u0434\u0443\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043e\u0431\u044a\u0435\u043a\u0442 `{replacement_entity_id}`.", + "title": "\u041e\u0431\u044a\u0435\u043a\u0442 {old_entity_id} \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d" + } + } + }, + "title": "\u041e\u0431\u044a\u0435\u043a\u0442 {old_entity_id} \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/rainmachine/translations/zh-Hant.json b/homeassistant/components/rainmachine/translations/zh-Hant.json index d37ae79541f..c0ead98a13e 100644 --- a/homeassistant/components/rainmachine/translations/zh-Hant.json +++ b/homeassistant/components/rainmachine/translations/zh-Hant.json @@ -18,6 +18,19 @@ } } }, + "issues": { + "replaced_old_entity": { + "fix_flow": { + "step": { + "confirm": { + "description": "\u4f7f\u7528\u6b64\u5be6\u9ad4\u4ee5\u66f4\u65b0\u4efb\u4f55\u81ea\u52d5\u5316\u6216\u8173\u672c\uff0c\u4ee5\u53d6\u4ee3 `{replacement_entity_id}`\u3002", + "title": "{old_entity_id} \u5be6\u9ad4\u5c07\u9032\u884c\u79fb\u9664" + } + } + }, + "title": "{old_entity_id} \u5be6\u9ad4\u5c07\u9032\u884c\u79fb\u9664" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/rainmachine/util.py b/homeassistant/components/rainmachine/util.py index dc772690ec5..67ffc83d5bd 100644 --- a/homeassistant/components/rainmachine/util.py +++ b/homeassistant/components/rainmachine/util.py @@ -1,13 +1,15 @@ """Define RainMachine utilities.""" from __future__ import annotations -from collections.abc import Awaitable, Callable +from collections.abc import Awaitable, Callable, Iterable +from dataclasses import dataclass from datetime import timedelta from typing import Any from homeassistant.backports.enum import StrEnum from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import entity_registry from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, @@ -35,6 +37,43 @@ RUN_STATE_MAP = { } +@dataclass +class EntityDomainReplacementStrategy: + """Define an entity replacement.""" + + old_domain: str + old_unique_id: str + replacement_entity_id: str + breaks_in_ha_version: str + remove_old_entity: bool = True + + +@callback +def async_finish_entity_domain_replacements( + hass: HomeAssistant, + entry: ConfigEntry, + entity_replacement_strategies: Iterable[EntityDomainReplacementStrategy], +) -> None: + """Remove old entities and create a repairs issue with info on their replacement.""" + ent_reg = entity_registry.async_get(hass) + for strategy in entity_replacement_strategies: + try: + [registry_entry] = [ + registry_entry + for registry_entry in ent_reg.entities.values() + if registry_entry.config_entry_id == entry.entry_id + and registry_entry.domain == strategy.old_domain + and registry_entry.unique_id == strategy.old_unique_id + ] + except ValueError: + continue + + old_entity_id = registry_entry.entity_id + if strategy.remove_old_entity: + LOGGER.info('Removing old entity: "%s"', old_entity_id) + ent_reg.async_remove(old_entity_id) + + def key_exists(data: dict[str, Any], search_key: str) -> bool: """Return whether a key exists in a nested dict.""" for key, value in data.items(): diff --git a/homeassistant/components/random/binary_sensor.py b/homeassistant/components/random/binary_sensor.py index 3a53e297d1f..5e688162124 100644 --- a/homeassistant/components/random/binary_sensor.py +++ b/homeassistant/components/random/binary_sensor.py @@ -63,7 +63,7 @@ class RandomSensor(BinarySensorEntity): """Return the sensor class of the sensor.""" return self._device_class - async def async_update(self): + async def async_update(self) -> None: """Get new state and update the sensor's state.""" self._state = bool(getrandbits(1)) diff --git a/homeassistant/components/random/sensor.py b/homeassistant/components/random/sensor.py index a877f5cf0a3..19cf403eab2 100644 --- a/homeassistant/components/random/sensor.py +++ b/homeassistant/components/random/sensor.py @@ -87,7 +87,7 @@ class RandomSensor(SensorEntity): """Return the attributes of the sensor.""" return {ATTR_MAXIMUM: self._maximum, ATTR_MINIMUM: self._minimum} - async def async_update(self): + async def async_update(self) -> None: """Get a new number and updates the states.""" self._state = randrange(self._minimum, self._maximum + 1) diff --git a/homeassistant/components/rdw/translations/cs.json b/homeassistant/components/rdw/translations/cs.json new file mode 100644 index 00000000000..5d403348397 --- /dev/null +++ b/homeassistant/components/rdw/translations/cs.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/recorder/core.py b/homeassistant/components/recorder/core.py index 30ece9e98a5..c0f19f2e864 100644 --- a/homeassistant/components/recorder/core.py +++ b/homeassistant/components/recorder/core.py @@ -13,6 +13,7 @@ import threading import time from typing import Any, TypeVar, cast +import async_timeout from awesomeversion import AwesomeVersion from lru import LRU # pylint: disable=no-name-in-module from sqlalchemy import create_engine, event as sqlalchemy_event, exc, func, select @@ -71,6 +72,7 @@ from .queries import find_shared_attributes_id, find_shared_data_id from .run_history import RunHistory from .tasks import ( AdjustStatisticsTask, + ChangeStatisticsUnitTask, ClearStatisticsTask, CommitTask, DatabaseLockTask, @@ -479,10 +481,18 @@ class Recorder(threading.Thread): @callback def async_adjust_statistics( - self, statistic_id: str, start_time: datetime, sum_adjustment: float + self, + statistic_id: str, + start_time: datetime, + sum_adjustment: float, + adjustment_unit: str, ) -> None: """Adjust statistics.""" - self.queue_task(AdjustStatisticsTask(statistic_id, start_time, sum_adjustment)) + self.queue_task( + AdjustStatisticsTask( + statistic_id, start_time, sum_adjustment, adjustment_unit + ) + ) @callback def async_clear_statistics(self, statistic_ids: list[str]) -> None: @@ -504,6 +514,21 @@ class Recorder(threading.Thread): ) ) + @callback + def async_change_statistics_unit( + self, + statistic_id: str, + *, + new_unit_of_measurement: str, + old_unit_of_measurement: str, + ) -> None: + """Change statistics unit for a statistic_id.""" + self.queue_task( + ChangeStatisticsUnitTask( + statistic_id, new_unit_of_measurement, old_unit_of_measurement + ) + ) + @callback def async_import_statistics( self, metadata: StatisticMetaData, stats: Iterable[StatisticData] @@ -615,6 +640,10 @@ class Recorder(threading.Thread): self.hass.add_job(self.async_set_db_ready) + # Catch up with missed statistics + with session_scope(session=self.get_session()) as session: + self._schedule_compile_missing_statistics(session) + _LOGGER.debug("Recorder processing the queue") self.hass.add_job(self._async_set_recorder_ready_migration_done) self._run_event_loop() @@ -1026,7 +1055,8 @@ class Recorder(threading.Thread): task = DatabaseLockTask(database_locked, threading.Event(), False) self.queue_task(task) try: - await asyncio.wait_for(database_locked.wait(), timeout=DB_LOCK_TIMEOUT) + async with async_timeout.timeout(DB_LOCK_TIMEOUT): + await database_locked.wait() except asyncio.TimeoutError as err: task.database_unlock.set() raise TimeoutError( @@ -1118,7 +1148,6 @@ class Recorder(threading.Thread): with session_scope(session=self.get_session()) as session: end_incomplete_runs(session, self.run_history.recording_start) self.run_history.start(session) - self._schedule_compile_missing_statistics(session) self._open_event_session() diff --git a/homeassistant/components/recorder/db_schema.py b/homeassistant/components/recorder/db_schema.py index 4777eeb500e..d76f89068d0 100644 --- a/homeassistant/components/recorder/db_schema.py +++ b/homeassistant/components/recorder/db_schema.py @@ -53,7 +53,7 @@ from .models import StatisticData, StatisticMetaData, process_timestamp # pylint: disable=invalid-name Base = declarative_base() -SCHEMA_VERSION = 29 +SCHEMA_VERSION = 30 _StatisticsBaseSelfT = TypeVar("_StatisticsBaseSelfT", bound="StatisticsBase") @@ -104,10 +104,10 @@ class FAST_PYSQLITE_DATETIME(sqlite.DATETIME): # type: ignore[misc] return lambda value: None if value is None else ciso8601.parse_datetime(value) -JSON_VARIENT_CAST = Text().with_variant( +JSON_VARIANT_CAST = Text().with_variant( postgresql.JSON(none_as_null=True), "postgresql" ) -JSONB_VARIENT_CAST = Text().with_variant( +JSONB_VARIANT_CAST = Text().with_variant( postgresql.JSONB(none_as_null=True), "postgresql" ) DATETIME_TYPE = ( @@ -590,17 +590,17 @@ class StatisticsRuns(Base): # type: ignore[misc,valid-type] EVENT_DATA_JSON = type_coerce( - EventData.shared_data.cast(JSONB_VARIENT_CAST), JSONLiteral(none_as_null=True) + EventData.shared_data.cast(JSONB_VARIANT_CAST), JSONLiteral(none_as_null=True) ) OLD_FORMAT_EVENT_DATA_JSON = type_coerce( - Events.event_data.cast(JSONB_VARIENT_CAST), JSONLiteral(none_as_null=True) + Events.event_data.cast(JSONB_VARIANT_CAST), JSONLiteral(none_as_null=True) ) SHARED_ATTRS_JSON = type_coerce( - StateAttributes.shared_attrs.cast(JSON_VARIENT_CAST), JSON(none_as_null=True) + StateAttributes.shared_attrs.cast(JSON_VARIANT_CAST), JSON(none_as_null=True) ) OLD_FORMAT_ATTRS_JSON = type_coerce( - States.attributes.cast(JSON_VARIENT_CAST), JSON(none_as_null=True) + States.attributes.cast(JSON_VARIANT_CAST), JSON(none_as_null=True) ) ENTITY_ID_IN_EVENT: Column = EVENT_DATA_JSON["entity_id"] diff --git a/homeassistant/components/recorder/history.py b/homeassistant/components/recorder/history.py index 7e875a5ff93..5c3f47c02ed 100644 --- a/homeassistant/components/recorder/history.py +++ b/homeassistant/components/recorder/history.py @@ -17,7 +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.const import ( +from homeassistant.components.websocket_api import ( COMPRESSED_STATE_LAST_UPDATED, COMPRESSED_STATE_STATE, ) @@ -36,8 +36,6 @@ from .models import ( ) from .util import execute_stmt_lambda_element, session_scope -# mypy: allow-untyped-defs, no-check-untyped-defs - _LOGGER = logging.getLogger(__name__) STATE_KEY = "state" diff --git a/homeassistant/components/recorder/manifest.json b/homeassistant/components/recorder/manifest.json index 9486d2eaf1e..19a22b2a1e3 100644 --- a/homeassistant/components/recorder/manifest.json +++ b/homeassistant/components/recorder/manifest.json @@ -2,8 +2,9 @@ "domain": "recorder", "name": "Recorder", "documentation": "https://www.home-assistant.io/integrations/recorder", - "requirements": ["sqlalchemy==1.4.40", "fnvhash==0.1.0"], + "requirements": ["sqlalchemy==1.4.41", "fnvhash==0.1.0"], "codeowners": ["@home-assistant/core"], "quality_scale": "internal", - "iot_class": "local_push" + "iot_class": "local_push", + "integration_type": "system" } diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index 6e4a67c9da5..f82ec7ba1eb 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -636,7 +636,7 @@ def _apply_update( # noqa: C901 fake_start_time += timedelta(minutes=5) # When querying the database, be careful to only explicitly query for columns - # which were present in schema version 21. If querying the table, SQLAlchemy + # which were present in schema version 22. If querying the table, SQLAlchemy # will refer to future columns. with session_scope(session=session_maker()) as session: for sum_statistic in session.query(StatisticsMeta.id).filter_by( @@ -747,6 +747,10 @@ def _apply_update( # noqa: C901 _create_index( session_maker, "statistics_meta", "ix_statistics_meta_statistic_id" ) + elif new_version == 30: + # This added a column to the statistics_meta table, removed again before + # release of HA Core 2022.10.0 + pass else: raise ValueError(f"No schema migration defined for version {new_version}") diff --git a/homeassistant/components/recorder/models.py b/homeassistant/components/recorder/models.py index ff53d9be3d1..cfc797cf7ea 100644 --- a/homeassistant/components/recorder/models.py +++ b/homeassistant/components/recorder/models.py @@ -7,7 +7,7 @@ from typing import Any, TypedDict, overload from sqlalchemy.engine.row import Row -from homeassistant.components.websocket_api.const import ( +from homeassistant.components.websocket_api import ( COMPRESSED_STATE_ATTRIBUTES, COMPRESSED_STATE_LAST_CHANGED, COMPRESSED_STATE_LAST_UPDATED, @@ -149,7 +149,7 @@ class LazyState(State): self.attr_cache = attr_cache @property # type: ignore[override] - def attributes(self) -> dict[str, Any]: # 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) @@ -161,7 +161,7 @@ class LazyState(State): self._attributes = value @property # type: ignore[override] - def context(self) -> Context: # type: ignore[override] + def context(self) -> Context: """State context.""" if self._context is None: self._context = Context(id=None) @@ -173,7 +173,7 @@ class LazyState(State): self._context = value @property # type: ignore[override] - def last_changed(self) -> datetime: # type: ignore[override] + def last_changed(self) -> datetime: """Last changed datetime.""" if self._last_changed is None: if (last_changed := self._row.last_changed) is not None: @@ -188,7 +188,7 @@ class LazyState(State): self._last_changed = value @property # type: ignore[override] - def last_updated(self) -> datetime: # type: ignore[override] + def last_updated(self) -> datetime: """Last updated datetime.""" if self._last_updated is None: self._last_updated = process_timestamp(self._row.last_updated) diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 0a9e29747a9..7ba5c5f8c73 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -6,13 +6,14 @@ from collections.abc import Callable, Iterable import contextlib import dataclasses from datetime import datetime, timedelta +from functools import partial from itertools import chain, groupby import json import logging import os import re from statistics import mean -from typing import TYPE_CHECKING, Any, Literal, overload +from typing import TYPE_CHECKING, Any, Literal from sqlalchemy import bindparam, func, lambda_stmt, select from sqlalchemy.engine.row import Row @@ -23,26 +24,35 @@ from sqlalchemy.sql.lambdas import StatementLambdaElement from sqlalchemy.sql.selectable import Subquery import voluptuous as vol -from homeassistant.const import ( - PRESSURE_PA, - TEMP_CELSIUS, - VOLUME_CUBIC_FEET, - VOLUME_CUBIC_METERS, -) +from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT from homeassistant.core import Event, HomeAssistant, callback, valid_entity_id from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry from homeassistant.helpers.json import JSONEncoder +from homeassistant.helpers.start import async_at_start from homeassistant.helpers.storage import STORAGE_DIR from homeassistant.helpers.typing import UNDEFINED, UndefinedType -import homeassistant.util.dt as dt_util -import homeassistant.util.pressure as pressure_util -import homeassistant.util.temperature as temperature_util -from homeassistant.util.unit_system import UnitSystem -import homeassistant.util.volume as volume_util +from homeassistant.util import dt as dt_util +from homeassistant.util.unit_conversion import ( + BaseUnitConverter, + DistanceConverter, + EnergyConverter, + MassConverter, + PowerConverter, + PressureConverter, + SpeedConverter, + TemperatureConverter, + VolumeConverter, +) from .const import DOMAIN, MAX_ROWS_TO_PURGE, SupportedDialect -from .db_schema import Statistics, StatisticsMeta, StatisticsRuns, StatisticsShortTerm +from .db_schema import ( + Statistics, + StatisticsBase, + StatisticsMeta, + StatisticsRuns, + StatisticsShortTerm, +) from .models import ( StatisticData, StatisticMetaData, @@ -104,13 +114,6 @@ QUERY_STATISTICS_SUMMARY_SUM = [ .label("rownum"), ] -QUERY_STATISTICS_SUMMARY_SUM_LEGACY = [ - StatisticsShortTerm.metadata_id, - StatisticsShortTerm.last_reset, - StatisticsShortTerm.state, - StatisticsShortTerm.sum, -] - QUERY_STATISTIC_META = [ StatisticsMeta.id, StatisticsMeta.statistic_id, @@ -121,46 +124,121 @@ QUERY_STATISTIC_META = [ StatisticsMeta.name, ] -QUERY_STATISTIC_META_ID = [ - StatisticsMeta.id, - StatisticsMeta.statistic_id, -] - -# Convert pressure, temperature and volume statistics from the normalized unit used for -# statistics to the unit configured by the user -STATISTIC_UNIT_TO_DISPLAY_UNIT_CONVERSIONS = { - PRESSURE_PA: lambda x, units: pressure_util.convert( - x, PRESSURE_PA, units.pressure_unit - ) - if x is not None - else None, - TEMP_CELSIUS: lambda x, units: temperature_util.convert( - x, TEMP_CELSIUS, units.temperature_unit - ) - if x is not None - else None, - VOLUME_CUBIC_METERS: lambda x, units: volume_util.convert( - x, VOLUME_CUBIC_METERS, _configured_unit(VOLUME_CUBIC_METERS, units) - ) - if x is not None - else None, +STATISTIC_UNIT_TO_UNIT_CONVERTER: dict[str | None, type[BaseUnitConverter]] = { + **{unit: DistanceConverter for unit in DistanceConverter.VALID_UNITS}, + **{unit: EnergyConverter for unit in EnergyConverter.VALID_UNITS}, + **{unit: MassConverter for unit in MassConverter.VALID_UNITS}, + **{unit: PowerConverter for unit in PowerConverter.VALID_UNITS}, + **{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: VolumeConverter for unit in VolumeConverter.VALID_UNITS}, } -# Convert volume statistics from the display unit configured by the user -# to the normalized unit used for statistics -# This is used to support adjusting statistics in the display unit -DISPLAY_UNIT_TO_STATISTIC_UNIT_CONVERSIONS: dict[ - str, Callable[[float, UnitSystem], float] -] = { - VOLUME_CUBIC_FEET: lambda x, units: volume_util.convert( - x, _configured_unit(VOLUME_CUBIC_METERS, units), VOLUME_CUBIC_METERS - ), -} _LOGGER = logging.getLogger(__name__) +def _get_unit_class(unit: str | None) -> str | None: + """Get corresponding unit class from from the statistics unit.""" + if converter := STATISTIC_UNIT_TO_UNIT_CONVERTER.get(unit): + return converter.UNIT_CLASS + return None + + +def _get_statistic_to_display_unit_converter( + statistic_unit: str | None, + state_unit: str | None, + requested_units: dict[str, str] | None, +) -> Callable[[float | None], float | None]: + """Prepare a converter from the statistics unit to display unit.""" + + def no_conversion(val: float | None) -> float | None: + """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 + + display_unit: str | None + unit_class = converter.UNIT_CLASS + if requested_units and unit_class in requested_units: + display_unit = requested_units[unit_class] + else: + display_unit = state_unit + + if display_unit not in converter.VALID_UNITS: + # Guard against invalid state unit in the DB + return no_conversion + + def from_normalized_unit( + val: float | None, conv: type[BaseUnitConverter], from_unit: str, to_unit: str + ) -> float | None: + """Return val.""" + if val is None: + return val + return conv.convert(val, from_unit=from_unit, to_unit=to_unit) + + return partial( + from_normalized_unit, + conv=converter, + from_unit=statistic_unit, + to_unit=display_unit, + ) + + +def _get_display_to_statistic_unit_converter( + display_unit: str | None, + statistic_unit: str | None, +) -> Callable[[float], float]: + """Prepare a converter from the display unit to the statistics unit.""" + + def no_conversion(val: float) -> float: + """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 + + return partial(converter.convert, from_unit=display_unit, to_unit=statistic_unit) + + +def _get_unit_converter( + from_unit: str, to_unit: str +) -> Callable[[float | None], float | None]: + """Prepare a converter from a unit to another unit.""" + + def convert_units( + val: float | None, conv: type[BaseUnitConverter], from_unit: str, to_unit: str + ) -> float | None: + """Return converted val.""" + if val is None: + return val + return conv.convert(val, from_unit=from_unit, to_unit=to_unit) + + for conv in STATISTIC_UNIT_TO_UNIT_CONVERTER.values(): + if from_unit in conv.VALID_UNITS and to_unit in conv.VALID_UNITS: + return partial( + convert_units, conv=conv, from_unit=from_unit, to_unit=to_unit + ) + raise HomeAssistantError + + +def can_convert_units(from_unit: str | None, to_unit: str | None) -> bool: + """Return True if it's possible to convert from from_unit to to_unit.""" + for converter in STATISTIC_UNIT_TO_UNIT_CONVERTER.values(): + if from_unit in converter.VALID_UNITS and to_unit in converter.VALID_UNITS: + return True + return False + + @dataclasses.dataclass class PlatformCompiledStatistics: """Compiled Statistics from a platform.""" @@ -222,13 +300,17 @@ def async_setup(hass: HomeAssistant) -> None: return True - if hass.is_running: + @callback + def setup_entity_registry_event_handler(hass: HomeAssistant) -> None: + """Subscribe to event registry events.""" hass.bus.async_listen( entity_registry.EVENT_ENTITY_REGISTRY_UPDATED, _async_entity_id_changed, event_filter=entity_registry_changed_filter, ) + async_at_start(hass, setup_entity_registry_event_handler) + def get_start_time() -> datetime: """Return start time.""" @@ -420,6 +502,9 @@ def delete_statistics_duplicates(hass: HomeAssistant, session: Session) -> None: def _find_statistics_meta_duplicates(session: Session) -> list[int]: """Find duplicated statistics_meta.""" + # When querying the database, be careful to only explicitly query for columns + # which were present in schema version 29. If querying the table, SQLAlchemy + # will refer to future columns. subquery = ( session.query( StatisticsMeta.statistic_id, @@ -430,7 +515,7 @@ def _find_statistics_meta_duplicates(session: Session) -> list[int]: .subquery() ) query = ( - session.query(StatisticsMeta) + session.query(StatisticsMeta.statistic_id, StatisticsMeta.id) .outerjoin( subquery, (subquery.c.statistic_id == StatisticsMeta.statistic_id), @@ -473,7 +558,10 @@ def _delete_statistics_meta_duplicates(session: Session) -> int: def delete_statistics_meta_duplicates(session: Session) -> None: - """Identify and delete duplicated statistics_meta.""" + """Identify and delete duplicated statistics_meta. + + This is used when migrating from schema version 28 to schema version 29. + """ deleted_statistics_rows = _delete_statistics_meta_duplicates(session) if deleted_statistics_rows: _LOGGER.info( @@ -731,12 +819,12 @@ def get_metadata_with_session( meta["statistic_id"]: ( meta["id"], { - "source": meta["source"], - "statistic_id": meta["statistic_id"], - "unit_of_measurement": meta["unit_of_measurement"], "has_mean": meta["has_mean"], "has_sum": meta["has_sum"], "name": meta["name"], + "source": meta["source"], + "statistic_id": meta["statistic_id"], + "unit_of_measurement": meta["unit_of_measurement"], }, ) for meta in result @@ -761,29 +849,6 @@ def get_metadata( ) -@overload -def _configured_unit(unit: None, units: UnitSystem) -> None: - ... - - -@overload -def _configured_unit(unit: str, units: UnitSystem) -> str: - ... - - -def _configured_unit(unit: str | None, units: UnitSystem) -> str | None: - """Return the pressure and temperature units configured by the user.""" - if unit == PRESSURE_PA: - return units.pressure_unit - if unit == TEMP_CELSIUS: - return units.temperature_unit - if unit == VOLUME_CUBIC_METERS: - if units.is_metric: - return VOLUME_CUBIC_METERS - return VOLUME_CUBIC_FEET - return unit - - def clear_statistics(instance: Recorder, statistic_ids: list[str]) -> None: """Clear statistics for a list of statistic_ids.""" with session_scope(session=instance.get_session()) as session: @@ -828,11 +893,6 @@ def list_statistic_ids( """ result = {} - def _display_unit(hass: HomeAssistant, unit: str | None) -> str | None: - if unit is None: - return None - return _configured_unit(unit, hass.config.units) - # Query the database with session_scope(hass=hass) as session: metadata = get_metadata_with_session( @@ -845,9 +905,7 @@ def list_statistic_ids( "has_sum": meta["has_sum"], "name": meta["name"], "source": meta["source"], - "display_unit_of_measurement": _display_unit( - hass, meta["unit_of_measurement"] - ), + "unit_class": _get_unit_class(meta["unit_of_measurement"]), "unit_of_measurement": meta["unit_of_measurement"], } for _, meta in metadata.values() @@ -869,9 +927,7 @@ def list_statistic_ids( "has_sum": meta["has_sum"], "name": meta["name"], "source": meta["source"], - "display_unit_of_measurement": _display_unit( - hass, meta["unit_of_measurement"] - ), + "unit_class": _get_unit_class(meta["unit_of_measurement"]), "unit_of_measurement": meta["unit_of_measurement"], } @@ -883,8 +939,8 @@ def list_statistic_ids( "has_sum": info["has_sum"], "name": info.get("name"), "source": info["source"], - "display_unit_of_measurement": info["display_unit_of_measurement"], "statistics_unit_of_measurement": info["unit_of_measurement"], + "unit_class": info["unit_class"], } for _id, info in result.items() ] @@ -1039,6 +1095,7 @@ def statistics_during_period( statistic_ids: list[str] | None = None, period: Literal["5minute", "day", "hour", "month"] = "hour", start_time_as_datetime: bool = False, + units: dict[str, str] | None = None, ) -> dict[str, list[dict[str, Any]]]: """Return statistics during UTC period start_time - end_time for the statistic_ids. @@ -1080,10 +1137,20 @@ def statistics_during_period( table, start_time, start_time_as_datetime, + units, ) result = _sorted_statistics_to_dict( - hass, session, stats, statistic_ids, metadata, True, table, start_time, True + hass, + session, + stats, + statistic_ids, + metadata, + True, + table, + start_time, + True, + units, ) if period == "day": @@ -1152,6 +1219,8 @@ def _get_last_statistics( convert_units, table, None, + False, + None, ) @@ -1236,6 +1305,8 @@ def get_latest_short_term_statistics( False, StatisticsShortTerm, None, + False, + None, ) @@ -1280,18 +1351,18 @@ def _sorted_statistics_to_dict( convert_units: bool, table: type[Statistics | StatisticsShortTerm], start_time: datetime | None, - start_time_as_datetime: bool = False, + start_time_as_datetime: bool, + units: dict[str, str] | None, ) -> dict[str, list[dict]]: """Convert SQL results into JSON friendly data structure.""" result: dict = defaultdict(list) - units = hass.config.units metadata = dict(_metadata.values()) need_stat_at_start_time: set[int] = set() stats_at_start_time = {} - def no_conversion(val: Any, _: Any) -> float | None: - """Return x.""" - return val # type: ignore[no-any-return] + def no_conversion(val: float | None) -> float | None: + """Return val.""" + return val # Set all statistic IDs to empty lists in result set to maintain the order if statistic_ids is not None: @@ -1314,11 +1385,12 @@ def _sorted_statistics_to_dict( # 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] - unit = metadata[meta_id]["unit_of_measurement"] + state_unit = unit = metadata[meta_id]["unit_of_measurement"] statistic_id = metadata[meta_id]["statistic_id"] - convert: Callable[[Any, Any], float | None] - if convert_units: - convert = STATISTIC_UNIT_TO_DISPLAY_UNIT_CONVERSIONS.get(unit, lambda x, units: x) # type: ignore[arg-type,no-any-return] + if state := hass.states.get(statistic_id): + state_unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + if unit is not None and convert_units: + convert = _get_statistic_to_display_unit_converter(unit, state_unit, units) else: convert = no_conversion ent_results = result[meta_id] @@ -1330,14 +1402,14 @@ def _sorted_statistics_to_dict( "statistic_id": statistic_id, "start": start if start_time_as_datetime else start.isoformat(), "end": end.isoformat(), - "mean": convert(db_state.mean, units), - "min": convert(db_state.min, units), - "max": convert(db_state.max, units), + "mean": convert(db_state.mean), + "min": convert(db_state.min), + "max": convert(db_state.max), "last_reset": process_timestamp_to_utc_isoformat( db_state.last_reset ), - "state": convert(db_state.state, units), - "sum": convert(db_state.sum, units), + "state": convert(db_state.state), + "sum": convert(db_state.sum), } ) @@ -1513,6 +1585,7 @@ def adjust_statistics( statistic_id: str, start_time: datetime, sum_adjustment: float, + adjustment_unit: str, ) -> bool: """Process an add_statistics job.""" @@ -1523,11 +1596,11 @@ def adjust_statistics( if statistic_id not in metadata: return True - units = instance.hass.config.units statistic_unit = metadata[statistic_id][1]["unit_of_measurement"] - display_unit = _configured_unit(statistic_unit, units) - convert = DISPLAY_UNIT_TO_STATISTIC_UNIT_CONVERSIONS.get(display_unit, lambda x, units: x) # type: ignore[arg-type] - sum_adjustment = convert(sum_adjustment, units) + convert = _get_display_to_statistic_unit_converter( + adjustment_unit, statistic_unit + ) + sum_adjustment = convert(sum_adjustment) _adjust_sum_statistics( session, @@ -1546,3 +1619,79 @@ def adjust_statistics( ) return True + + +def _change_statistics_unit_for_table( + session: Session, + table: type[StatisticsBase], + metadata_id: int, + convert: Callable[[float | None], float | None], +) -> None: + """Insert statistics in the database.""" + columns = [table.id, table.mean, table.min, table.max, table.state, table.sum] + query = session.query(*columns).filter_by(metadata_id=bindparam("metadata_id")) + rows = execute(query.params(metadata_id=metadata_id)) + for row in rows: + session.query(table).filter(table.id == row.id).update( + { + table.mean: convert(row.mean), + table.min: convert(row.min), + table.max: convert(row.max), + table.state: convert(row.state), + table.sum: convert(row.sum), + }, + synchronize_session=False, + ) + + +def change_statistics_unit( + instance: Recorder, + statistic_id: str, + new_unit: str, + old_unit: str, +) -> None: + """Change statistics unit for a statistic_id.""" + with session_scope(session=instance.get_session()) as session: + metadata = get_metadata_with_session( + instance.hass, session, statistic_ids=(statistic_id,) + ).get(statistic_id) + + # Guard against the statistics being removed or updated before the + # change_statistics_unit job executes + if ( + metadata is None + or metadata[1]["source"] != DOMAIN + or metadata[1]["unit_of_measurement"] != old_unit + ): + _LOGGER.warning("Could not change statistics unit for %s", statistic_id) + return + + metadata_id = metadata[0] + + convert = _get_unit_converter(old_unit, new_unit) + for table in (StatisticsShortTerm, Statistics): + _change_statistics_unit_for_table(session, table, metadata_id, convert) + session.query(StatisticsMeta).filter( + StatisticsMeta.statistic_id == statistic_id + ).update({StatisticsMeta.unit_of_measurement: new_unit}) + + +@callback +def async_change_statistics_unit( + hass: HomeAssistant, + statistic_id: str, + *, + new_unit_of_measurement: str, + old_unit_of_measurement: str, +) -> None: + """Change statistics unit for a statistic_id.""" + if not can_convert_units(old_unit_of_measurement, new_unit_of_measurement): + raise HomeAssistantError( + f"Can't convert {old_unit_of_measurement} to {new_unit_of_measurement}" + ) + + get_instance(hass).async_change_statistics_unit( + statistic_id, + new_unit_of_measurement=new_unit_of_measurement, + old_unit_of_measurement=old_unit_of_measurement, + ) diff --git a/homeassistant/components/recorder/system_health/__init__.py b/homeassistant/components/recorder/system_health/__init__.py index c4bf2c3bb89..b79f526db2b 100644 --- a/homeassistant/components/recorder/system_health/__init__.py +++ b/homeassistant/components/recorder/system_health/__init__.py @@ -5,12 +5,12 @@ from typing import Any from urllib.parse import urlparse from homeassistant.components import system_health -from homeassistant.components.recorder.core import Recorder -from homeassistant.components.recorder.util import session_scope from homeassistant.core import HomeAssistant, callback from .. import get_instance from ..const import SupportedDialect +from ..core import Recorder +from ..util import session_scope from .mysql import db_size_bytes as mysql_db_size_bytes from .postgresql import db_size_bytes as postgresql_db_size_bytes from .sqlite import db_size_bytes as sqlite_db_size_bytes diff --git a/homeassistant/components/recorder/tasks.py b/homeassistant/components/recorder/tasks.py index cdb97d9d67c..4fa3a3cc40c 100644 --- a/homeassistant/components/recorder/tasks.py +++ b/homeassistant/components/recorder/tasks.py @@ -31,6 +31,24 @@ class RecorderTask(abc.ABC): """Handle the task.""" +@dataclass +class ChangeStatisticsUnitTask(RecorderTask): + """Object to store statistics_id and unit to convert unit of statistics.""" + + statistic_id: str + new_unit_of_measurement: str + old_unit_of_measurement: str + + def run(self, instance: Recorder) -> None: + """Handle the task.""" + statistics.change_statistics_unit( + instance, + self.statistic_id, + self.new_unit_of_measurement, + self.old_unit_of_measurement, + ) + + @dataclass class ClearStatisticsTask(RecorderTask): """Object to store statistics_ids which for which to remove statistics.""" @@ -145,6 +163,7 @@ class AdjustStatisticsTask(RecorderTask): statistic_id: str start_time: datetime sum_adjustment: float + adjustment_unit: str def run(self, instance: Recorder) -> None: """Run statistics task.""" @@ -153,12 +172,16 @@ class AdjustStatisticsTask(RecorderTask): self.statistic_id, self.start_time, self.sum_adjustment, + self.adjustment_unit, ): return # Schedule a new adjust statistics task if this one didn't finish instance.queue_task( AdjustStatisticsTask( - self.statistic_id, self.start_time, self.sum_adjustment + self.statistic_id, + self.start_time, + self.sum_adjustment, + self.adjustment_unit, ) ) diff --git a/homeassistant/components/recorder/websocket_api.py b/homeassistant/components/recorder/websocket_api.py index 16813944780..c583577ec8f 100644 --- a/homeassistant/components/recorder/websocket_api.py +++ b/homeassistant/components/recorder/websocket_api.py @@ -1,20 +1,37 @@ """The Recorder websocket API.""" from __future__ import annotations +from datetime import datetime as dt import logging +from typing import Literal import voluptuous as vol from homeassistant.components import websocket_api +from homeassistant.components.websocket_api import messages from homeassistant.core import HomeAssistant, callback, valid_entity_id from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.json import JSON_DUMP from homeassistant.util import dt as dt_util +from homeassistant.util.unit_conversion import ( + DistanceConverter, + EnergyConverter, + MassConverter, + PowerConverter, + PressureConverter, + SpeedConverter, + TemperatureConverter, + VolumeConverter, +) from .const import MAX_QUEUE_BACKLOG from .statistics import ( + STATISTIC_UNIT_TO_UNIT_CONVERTER, async_add_external_statistics, + async_change_statistics_unit, async_import_statistics, list_statistic_ids, + statistics_during_period, validate_statistics, ) from .util import async_migration_in_progress, async_migration_is_live, get_instance @@ -25,15 +42,142 @@ _LOGGER: logging.Logger = logging.getLogger(__package__) @callback def async_setup(hass: HomeAssistant) -> None: """Set up the recorder websocket API.""" - websocket_api.async_register_command(hass, ws_validate_statistics) - websocket_api.async_register_command(hass, ws_clear_statistics) - websocket_api.async_register_command(hass, ws_get_statistics_metadata) - websocket_api.async_register_command(hass, ws_update_statistics_metadata) - websocket_api.async_register_command(hass, ws_info) - websocket_api.async_register_command(hass, ws_backup_start) - websocket_api.async_register_command(hass, ws_backup_end) websocket_api.async_register_command(hass, ws_adjust_sum_statistics) + websocket_api.async_register_command(hass, ws_backup_end) + websocket_api.async_register_command(hass, ws_backup_start) + websocket_api.async_register_command(hass, ws_change_statistics_unit) + websocket_api.async_register_command(hass, ws_clear_statistics) + websocket_api.async_register_command(hass, ws_get_statistics_during_period) + websocket_api.async_register_command(hass, ws_get_statistics_metadata) + websocket_api.async_register_command(hass, ws_list_statistic_ids) websocket_api.async_register_command(hass, ws_import_statistics) + websocket_api.async_register_command(hass, ws_info) + websocket_api.async_register_command(hass, ws_update_statistics_metadata) + websocket_api.async_register_command(hass, ws_validate_statistics) + + +def _ws_get_statistics_during_period( + hass: HomeAssistant, + msg_id: int, + start_time: dt, + end_time: dt | None, + statistic_ids: list[str] | None, + period: Literal["5minute", "day", "hour", "month"], + units: dict[str, str], +) -> str: + """Fetch statistics and convert them to json in the executor.""" + return JSON_DUMP( + messages.result_message( + msg_id, + statistics_during_period( + hass, start_time, end_time, statistic_ids, period, units=units + ), + ) + ) + + +async def ws_handle_get_statistics_during_period( + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict +) -> None: + """Handle statistics 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 + + connection.send_message( + await get_instance(hass).async_add_executor_job( + _ws_get_statistics_during_period, + hass, + msg["id"], + start_time, + end_time, + msg.get("statistic_ids"), + msg.get("period"), + msg.get("units"), + ) + ) + + +@websocket_api.websocket_command( + { + vol.Required("type"): "recorder/statistics_during_period", + vol.Required("start_time"): str, + vol.Optional("end_time"): str, + vol.Optional("statistic_ids"): [str], + vol.Required("period"): vol.Any("5minute", "hour", "day", "month"), + vol.Optional("units"): vol.Schema( + { + vol.Optional("distance"): vol.In(DistanceConverter.VALID_UNITS), + vol.Optional("energy"): vol.In(EnergyConverter.VALID_UNITS), + vol.Optional("mass"): vol.In(MassConverter.VALID_UNITS), + vol.Optional("power"): vol.In(PowerConverter.VALID_UNITS), + vol.Optional("pressure"): vol.In(PressureConverter.VALID_UNITS), + vol.Optional("speed"): vol.In(SpeedConverter.VALID_UNITS), + vol.Optional("temperature"): vol.In(TemperatureConverter.VALID_UNITS), + vol.Optional("volume"): vol.In(VolumeConverter.VALID_UNITS), + } + ), + } +) +@websocket_api.async_response +async def ws_get_statistics_during_period( + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict +) -> None: + """Handle statistics websocket command.""" + await ws_handle_get_statistics_during_period(hass, connection, msg) + + +def _ws_get_list_statistic_ids( + hass: HomeAssistant, + 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.""" + return JSON_DUMP( + messages.result_message(msg_id, list_statistic_ids(hass, None, statistic_type)) + ) + + +async def ws_handle_list_statistic_ids( + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict +) -> None: + """Fetch a list of available statistic_id.""" + connection.send_message( + await get_instance(hass).async_add_executor_job( + _ws_get_list_statistic_ids, + hass, + msg["id"], + msg.get("statistic_type"), + ) + ) + + +@websocket_api.websocket_command( + { + vol.Required("type"): "recorder/list_statistic_ids", + vol.Optional("statistic_type"): vol.Any("sum", "mean"), + } +) +@websocket_api.async_response +async def ws_list_statistic_ids( + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict +) -> None: + """Fetch a list of available statistic_id.""" + await ws_handle_list_statistic_ids(hass, connection, msg) @websocket_api.websocket_command( @@ -104,13 +248,42 @@ async def ws_get_statistics_metadata( def ws_update_statistics_metadata( hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict ) -> None: - """Update statistics metadata for a statistic_id.""" + """Update statistics metadata for a statistic_id. + + Only the normalized unit of measurement can be updated. + """ get_instance(hass).async_update_statistics_metadata( msg["statistic_id"], new_unit_of_measurement=msg["unit_of_measurement"] ) connection.send_result(msg["id"]) +@websocket_api.require_admin +@websocket_api.websocket_command( + { + vol.Required("type"): "recorder/change_statistics_unit", + vol.Required("statistic_id"): str, + vol.Required("new_unit_of_measurement"): vol.Any(str, None), + vol.Required("old_unit_of_measurement"): vol.Any(str, None), + } +) +@callback +def ws_change_statistics_unit( + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict +) -> None: + """Change the unit_of_measurement for a statistic_id. + + All existing statistics will be converted to the new unit. + """ + async_change_statistics_unit( + hass, + msg["statistic_id"], + new_unit_of_measurement=msg["new_unit_of_measurement"], + old_unit_of_measurement=msg["old_unit_of_measurement"], + ) + connection.send_result(msg["id"]) + + @websocket_api.require_admin @websocket_api.websocket_command( { @@ -118,13 +291,18 @@ def ws_update_statistics_metadata( vol.Required("statistic_id"): str, vol.Required("start_time"): str, vol.Required("adjustment"): vol.Any(float, int), + vol.Required("adjustment_unit_of_measurement"): vol.Any(str, None), } ) -@callback -def ws_adjust_sum_statistics( +@websocket_api.async_response +async def ws_adjust_sum_statistics( hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict ) -> None: - """Adjust sum statistics.""" + """Adjust sum statistics. + + If the statistics is stored as NORMALIZED_UNIT, + it's allowed to make an adjustment in VALID_UNIT + """ start_time_str = msg["start_time"] if start_time := dt_util.parse_datetime(start_time_str): @@ -133,8 +311,35 @@ def ws_adjust_sum_statistics( connection.send_error(msg["id"], "invalid_start_time", "Invalid start time") return + instance = get_instance(hass) + metadatas = await instance.async_add_executor_job( + list_statistic_ids, hass, (msg["statistic_id"],) + ) + if not metadatas: + connection.send_error(msg["id"], "unknown_statistic_id", "Unknown statistic ID") + return + metadata = metadatas[0] + + def valid_units(statistics_unit: str | None, adjustment_unit: str | None) -> bool: + if statistics_unit == adjustment_unit: + return True + converter = STATISTIC_UNIT_TO_UNIT_CONVERTER.get(statistics_unit) + if converter is not None and adjustment_unit in converter.VALID_UNITS: + return True + return False + + stat_unit = metadata["statistics_unit_of_measurement"] + adjustment_unit = msg["adjustment_unit_of_measurement"] + if not valid_units(stat_unit, adjustment_unit): + connection.send_error( + msg["id"], + "invalid_units", + f"Can't convert {stat_unit} to {adjustment_unit}", + ) + return + get_instance(hass).async_adjust_statistics( - msg["statistic_id"], start_time, msg["adjustment"] + msg["statistic_id"], start_time, msg["adjustment"], adjustment_unit ) connection.send_result(msg["id"]) @@ -168,7 +373,7 @@ def ws_adjust_sum_statistics( def ws_import_statistics( hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict ) -> None: - """Adjust sum statistics.""" + """Import statistics.""" metadata = msg["metadata"] stats = msg["stats"] diff --git a/homeassistant/components/recswitch/switch.py b/homeassistant/components/recswitch/switch.py index cc98a922f50..43e71ef1df9 100644 --- a/homeassistant/components/recswitch/switch.py +++ b/homeassistant/components/recswitch/switch.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any from pyrecswitch import RSNetwork, RSNetworkError import voluptuous as vol @@ -76,11 +77,11 @@ class RecSwitchSwitch(SwitchEntity): """Return true if switch is on.""" return self.gpio_state - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn on the switch.""" await self.async_set_gpio_status(True) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn off the switch.""" await self.async_set_gpio_status(False) @@ -93,7 +94,7 @@ class RecSwitchSwitch(SwitchEntity): except RSNetworkError as error: _LOGGER.error("Setting status to %s: %r", self.name, error) - async def async_update(self): + async def async_update(self) -> None: """Update the current switch status.""" try: diff --git a/homeassistant/components/reddit/sensor.py b/homeassistant/components/reddit/sensor.py index 0f4638634ce..0ae53ca8610 100644 --- a/homeassistant/components/reddit/sensor.py +++ b/homeassistant/components/reddit/sensor.py @@ -127,7 +127,7 @@ class RedditSensor(SensorEntity): """Return the icon to use in the frontend.""" return "mdi:reddit" - def update(self): + def update(self) -> None: """Update data from Reddit API.""" self._subreddit_data = [] diff --git a/homeassistant/components/rejseplanen/sensor.py b/homeassistant/components/rejseplanen/sensor.py index a6251bf62e7..6943e7f8aa3 100644 --- a/homeassistant/components/rejseplanen/sensor.py +++ b/homeassistant/components/rejseplanen/sensor.py @@ -150,7 +150,7 @@ class RejseplanenTransportSensor(SensorEntity): """Icon to use in the frontend, if any.""" return ICON - def update(self): + def update(self) -> None: """Get the latest data from rejseplanen.dk and update the states.""" self.data.update() self._times = self.data.info diff --git a/homeassistant/components/remember_the_milk/__init__.py b/homeassistant/components/remember_the_milk/__init__.py index fbc2518ce1b..3331f9c61d4 100644 --- a/homeassistant/components/remember_the_milk/__init__.py +++ b/homeassistant/components/remember_the_milk/__init__.py @@ -52,7 +52,7 @@ SERVICE_SCHEMA_COMPLETE_TASK = vol.Schema({vol.Required(CONF_ID): cv.string}) def setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Remember the milk component.""" - component = EntityComponent(_LOGGER, DOMAIN, hass) + component = EntityComponent[RememberTheMilk](_LOGGER, DOMAIN, hass) stored_rtm_config = RememberTheMilkConfiguration(hass) for rtm_config in config[DOMAIN]: diff --git a/homeassistant/components/remote/__init__.py b/homeassistant/components/remote/__init__.py index 1a739a2a476..6ba5ca89d2d 100644 --- a/homeassistant/components/remote/__init__.py +++ b/homeassistant/components/remote/__init__.py @@ -7,7 +7,7 @@ from datetime import timedelta from enum import IntEnum import functools as ft import logging -from typing import Any, cast, final +from typing import Any, final import voluptuous as vol @@ -31,8 +31,6 @@ from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass -# mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs - _LOGGER = logging.getLogger(__name__) ATTR_ACTIVITY = "activity" @@ -90,7 +88,7 @@ def is_on(hass: HomeAssistant, entity_id: str) -> bool: async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Track states and offer events for remotes.""" - component = hass.data[DOMAIN] = EntityComponent( + component = hass.data[DOMAIN] = EntityComponent[RemoteEntity]( _LOGGER, DOMAIN, hass, SCAN_INTERVAL ) await component.async_setup(config) @@ -147,12 +145,14 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry.""" - return await cast(EntityComponent, hass.data[DOMAIN]).async_setup_entry(entry) + component: EntityComponent[RemoteEntity] = hass.data[DOMAIN] + return await component.async_setup_entry(entry) async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - return await cast(EntityComponent, hass.data[DOMAIN]).async_unload_entry(entry) + component: EntityComponent[RemoteEntity] = hass.data[DOMAIN] + return await component.async_unload_entry(entry) @dataclass diff --git a/homeassistant/components/remote/manifest.json b/homeassistant/components/remote/manifest.json index d08cb624c16..dac51e27749 100644 --- a/homeassistant/components/remote/manifest.json +++ b/homeassistant/components/remote/manifest.json @@ -3,5 +3,6 @@ "name": "Remote", "documentation": "https://www.home-assistant.io/integrations/remote", "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "entity" } diff --git a/homeassistant/components/remote_rpi_gpio/binary_sensor.py b/homeassistant/components/remote_rpi_gpio/binary_sensor.py index 9af1a83b2e9..37994830c4d 100644 --- a/homeassistant/components/remote_rpi_gpio/binary_sensor.py +++ b/homeassistant/components/remote_rpi_gpio/binary_sensor.py @@ -75,7 +75,7 @@ class RemoteRPiGPIOBinarySensor(BinarySensorEntity): self._state = False self._sensor = sensor - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Run when entity about to be added to hass.""" def read_gpio(): @@ -101,7 +101,7 @@ class RemoteRPiGPIOBinarySensor(BinarySensorEntity): """Return the class of this sensor, from DEVICE_CLASSES.""" return - def update(self): + def update(self) -> None: """Update the GPIO state.""" try: self._state = remote_rpi_gpio.read_input(self._sensor) diff --git a/homeassistant/components/remote_rpi_gpio/switch.py b/homeassistant/components/remote_rpi_gpio/switch.py index 9e7aca37663..862efb0f89d 100644 --- a/homeassistant/components/remote_rpi_gpio/switch.py +++ b/homeassistant/components/remote_rpi_gpio/switch.py @@ -1,6 +1,8 @@ """Allows to configure a switch using RPi GPIO.""" from __future__ import annotations +from typing import Any + import voluptuous as vol from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity @@ -75,13 +77,13 @@ class RemoteRPiGPIOSwitch(SwitchEntity): """Return true if device is on.""" return self._state - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" remote_rpi_gpio.write_output(self._switch, 1) self._state = True self.schedule_update_ha_state() - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" remote_rpi_gpio.write_output(self._switch, 0) self._state = False diff --git a/homeassistant/components/renault/sensor.py b/homeassistant/components/renault/sensor.py index 7e692182ff9..3076dfc9f10 100644 --- a/homeassistant/components/renault/sensor.py +++ b/homeassistant/components/renault/sensor.py @@ -225,6 +225,7 @@ SENSOR_TYPES: tuple[RenaultSensorEntityDescription[Any], ...] = ( key="battery_autonomy", coordinator="battery", data_key="batteryAutonomy", + device_class=SensorDeviceClass.DISTANCE, entity_class=RenaultSensor[KamereonVehicleBatteryStatusData], icon="mdi:ev-station", name="Battery autonomy", @@ -265,6 +266,7 @@ SENSOR_TYPES: tuple[RenaultSensorEntityDescription[Any], ...] = ( key="mileage", coordinator="cockpit", data_key="totalMileage", + device_class=SensorDeviceClass.DISTANCE, entity_class=RenaultSensor[KamereonVehicleCockpitData], icon="mdi:sign-direction", name="Mileage", @@ -276,6 +278,7 @@ SENSOR_TYPES: tuple[RenaultSensorEntityDescription[Any], ...] = ( key="fuel_autonomy", coordinator="cockpit", data_key="fuelAutonomy", + device_class=SensorDeviceClass.DISTANCE, entity_class=RenaultSensor[KamereonVehicleCockpitData], icon="mdi:gas-station", name="Fuel autonomy", @@ -288,6 +291,7 @@ SENSOR_TYPES: tuple[RenaultSensorEntityDescription[Any], ...] = ( key="fuel_quantity", coordinator="cockpit", data_key="fuelQuantity", + device_class=SensorDeviceClass.VOLUME, entity_class=RenaultSensor[KamereonVehicleCockpitData], icon="mdi:fuel", name="Fuel quantity", diff --git a/homeassistant/components/renault/translations/es.json b/homeassistant/components/renault/translations/es.json index 75a0ca557c5..fc110deb15b 100644 --- a/homeassistant/components/renault/translations/es.json +++ b/homeassistant/components/renault/translations/es.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", "kamereon_no_account": "No se puede encontrar la cuenta de Kamereon", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "error": { "invalid_credentials": "Autenticaci\u00f3n no v\u00e1lida" diff --git a/homeassistant/components/repairs/manifest.json b/homeassistant/components/repairs/manifest.json index f2013743a05..c63c3ec2946 100644 --- a/homeassistant/components/repairs/manifest.json +++ b/homeassistant/components/repairs/manifest.json @@ -4,5 +4,7 @@ "config_flow": false, "documentation": "https://www.home-assistant.io/integrations/repairs", "codeowners": ["@home-assistant/core"], - "dependencies": ["http"] + "dependencies": ["http"], + "quality_scale": "internal", + "integration_type": "system" } diff --git a/homeassistant/components/rest/__init__.py b/homeassistant/components/rest/__init__.py index f8e6941572a..96ed1ada7ae 100644 --- a/homeassistant/components/rest/__init__.py +++ b/homeassistant/components/rest/__init__.py @@ -1,6 +1,7 @@ """The rest component.""" import asyncio +import contextlib import logging import httpx @@ -26,12 +27,13 @@ from homeassistant.const import ( Platform, ) from homeassistant.core import HomeAssistant, ServiceCall, callback +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import discovery, template -from homeassistant.helpers.entity_component import ( - DEFAULT_SCAN_INTERVAL, - EntityComponent, +from homeassistant.helpers.entity_component import DEFAULT_SCAN_INTERVAL +from homeassistant.helpers.reload import ( + async_integration_yaml_config, + async_reload_integration_platforms, ) -from homeassistant.helpers.reload import async_reload_integration_platforms from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator @@ -53,12 +55,14 @@ COORDINATOR_AWARE_PLATFORMS = [SENSOR_DOMAIN, BINARY_SENSOR_DOMAIN] async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the rest platforms.""" - component = EntityComponent(_LOGGER, DOMAIN, hass) _async_setup_shared_data(hass) async def reload_service_handler(service: ServiceCall) -> None: """Remove all user-defined groups and load new ones from config.""" - if (conf := await component.async_prepare_reload()) is None: + conf = None + with contextlib.suppress(HomeAssistantError): + conf = await async_integration_yaml_config(hass, DOMAIN) + if conf is None: return await async_reload_integration_platforms(hass, DOMAIN, PLATFORMS) _async_setup_shared_data(hass) diff --git a/homeassistant/components/rest/data.py b/homeassistant/components/rest/data.py index 7c8fd61e688..86219634027 100644 --- a/homeassistant/components/rest/data.py +++ b/homeassistant/components/rest/data.py @@ -69,6 +69,12 @@ class RestData: ) self.data = response.text self.headers = response.headers + except httpx.TimeoutException as ex: + if log_errors: + _LOGGER.error("Timeout while fetching data: %s", self._resource) + self.last_exception = ex + self.data = None + self.headers = None except httpx.RequestError as ex: if log_errors: _LOGGER.error( diff --git a/homeassistant/components/rest/entity.py b/homeassistant/components/rest/entity.py index 5d7a65b3d48..f0cccc8b762 100644 --- a/homeassistant/components/rest/entity.py +++ b/homeassistant/components/rest/entity.py @@ -1,10 +1,12 @@ """The base entity for the rest component.""" +from __future__ import annotations from abc import abstractmethod from typing import Any from homeassistant.core import callback from homeassistant.helpers.entity import Entity +from homeassistant.helpers.template import Template from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .data import RestData @@ -15,31 +17,22 @@ class RestEntity(Entity): def __init__( self, - coordinator: DataUpdateCoordinator[Any], + coordinator: DataUpdateCoordinator[Any] | None, rest: RestData, - resource_template, - force_update, + resource_template: Template | None, + force_update: bool, ) -> None: """Create the entity that may have a coordinator.""" - self.coordinator = coordinator + self._coordinator = coordinator self.rest = rest self._resource_template = resource_template - self._force_update = force_update + self._attr_should_poll = not coordinator + self._attr_force_update = force_update @property - def force_update(self): - """Force update.""" - return self._force_update - - @property - def should_poll(self) -> bool: - """Poll only if we do not have a coordinator.""" - return not self.coordinator - - @property - def available(self): + def available(self) -> bool: """Return the availability of this sensor.""" - if self.coordinator and not self.coordinator.last_update_success: + if self._coordinator and not self._coordinator.last_update_success: return False return self.rest.data is not None @@ -47,9 +40,9 @@ class RestEntity(Entity): """When entity is added to hass.""" await super().async_added_to_hass() self._update_from_rest_data() - if self.coordinator: + if self._coordinator: self.async_on_remove( - self.coordinator.async_add_listener(self._handle_coordinator_update) + self._coordinator.async_add_listener(self._handle_coordinator_update) ) @callback @@ -58,10 +51,10 @@ class RestEntity(Entity): self._update_from_rest_data() self.async_write_ha_state() - async def async_update(self): + async def async_update(self) -> None: """Get the latest data from REST API and update the state.""" - if self.coordinator: - await self.coordinator.async_request_refresh() + if self._coordinator: + await self._coordinator.async_request_refresh() return if self._resource_template is not None: @@ -70,5 +63,5 @@ class RestEntity(Entity): self._update_from_rest_data() @abstractmethod - def _update_from_rest_data(self): + def _update_from_rest_data(self) -> None: """Update state from the rest data.""" diff --git a/homeassistant/components/rest/switch.py b/homeassistant/components/rest/switch.py index c45eb581645..f2a5d93cd22 100644 --- a/homeassistant/components/rest/switch.py +++ b/homeassistant/components/rest/switch.py @@ -4,6 +4,7 @@ from __future__ import annotations import asyncio from http import HTTPStatus import logging +from typing import Any import aiohttp import async_timeout @@ -153,7 +154,7 @@ class RestSwitch(TemplateEntity, SwitchEntity): """Return true if device is on.""" return self._state - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" body_on_t = self._body_on.async_render(parse_result=False) @@ -169,7 +170,7 @@ class RestSwitch(TemplateEntity, SwitchEntity): except (asyncio.TimeoutError, aiohttp.ClientError): _LOGGER.error("Error while switching on %s", self._resource) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" body_off_t = self._body_off.async_render(parse_result=False) @@ -201,7 +202,7 @@ class RestSwitch(TemplateEntity, SwitchEntity): ) return req - async def async_update(self): + async def async_update(self) -> None: """Get the current state, catching errors.""" try: await self.get_device_state(self.hass) diff --git a/homeassistant/components/rflink/binary_sensor.py b/homeassistant/components/rflink/binary_sensor.py index ce723e84b8c..e307d9de382 100644 --- a/homeassistant/components/rflink/binary_sensor.py +++ b/homeassistant/components/rflink/binary_sensor.py @@ -1,11 +1,14 @@ """Support for Rflink binary sensors.""" from __future__ import annotations +from typing import Any + import voluptuous as vol from homeassistant.components.binary_sensor import ( DEVICE_CLASSES_SCHEMA, PLATFORM_SCHEMA, + BinarySensorDeviceClass, BinarySensorEntity, ) from homeassistant.const import ( @@ -73,17 +76,22 @@ class RflinkBinarySensor(RflinkDevice, BinarySensorEntity, RestoreEntity): """Representation of an Rflink binary sensor.""" def __init__( - self, device_id, device_class=None, force_update=False, off_delay=None, **kwargs - ): + self, + device_id: str, + device_class: BinarySensorDeviceClass | None = None, + force_update: bool = False, + off_delay: int | None = None, + **kwargs: Any, + ) -> None: """Handle sensor specific args and super init.""" self._state = None - self._device_class = device_class - self._force_update = force_update + self._attr_device_class = device_class + self._attr_force_update = force_update self._off_delay = off_delay self._delay_listener = None super().__init__(device_id, **kwargs) - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Restore RFLink BinarySensor state.""" await super().async_added_to_hass() if (old_state := await self.async_get_last_state()) is not None: @@ -119,13 +127,3 @@ class RflinkBinarySensor(RflinkDevice, BinarySensorEntity, RestoreEntity): def is_on(self): """Return true if the binary sensor is on.""" return self._state - - @property - def device_class(self): - """Return the class of this sensor.""" - return self._device_class - - @property - def force_update(self): - """Force update.""" - return self._force_update diff --git a/homeassistant/components/rflink/sensor.py b/homeassistant/components/rflink/sensor.py index 5aac1f6debe..2420e933653 100644 --- a/homeassistant/components/rflink/sensor.py +++ b/homeassistant/components/rflink/sensor.py @@ -123,7 +123,7 @@ class RflinkSensor(RflinkDevice, SensorEntity): """Domain specific event handler.""" self._state = event["value"] - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register update callback.""" # Remove temporary bogus entity_id if added tmp_entity = TMP_ENTITY.format(self._device_id) diff --git a/homeassistant/components/rfxtrx/__init__.py b/homeassistant/components/rfxtrx/__init__.py index 3257a482c0c..d0d3793fd82 100644 --- a/homeassistant/components/rfxtrx/__init__.py +++ b/homeassistant/components/rfxtrx/__init__.py @@ -3,10 +3,10 @@ from __future__ import annotations import asyncio import binascii -from collections.abc import Callable +from collections.abc import Callable, Mapping import copy import logging -from typing import NamedTuple, cast +from typing import Any, NamedTuple, cast import RFXtrx as rfxtrxmod import async_timeout @@ -61,7 +61,7 @@ class DeviceTuple(NamedTuple): id_string: str -def _bytearray_string(data): +def _bytearray_string(data: Any) -> bytearray: val = cv.string(data) try: return bytearray.fromhex(val) @@ -116,7 +116,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -def _create_rfx(config): +def _create_rfx(config: Mapping[str, Any]) -> rfxtrxmod.Connect: """Construct a rfx object based on config.""" modes = config.get(CONF_PROTOCOLS) @@ -144,7 +144,9 @@ def _create_rfx(config): return rfx -def _get_device_lookup(devices): +def _get_device_lookup( + devices: dict[str, dict[str, Any]] +) -> dict[DeviceTuple, dict[str, Any]]: """Get a lookup structure for devices.""" lookup = {} for event_code, event_config in devices.items(): @@ -157,7 +159,7 @@ def _get_device_lookup(devices): return lookup -async def async_setup_internal(hass, entry: ConfigEntry): +async def async_setup_internal(hass: HomeAssistant, entry: ConfigEntry) -> None: """Set up the RFXtrx component.""" config = entry.data @@ -173,7 +175,7 @@ async def async_setup_internal(hass, entry: ConfigEntry): # Declare the Handle event @callback - def async_handle_receive(event): + def async_handle_receive(event: rfxtrxmod.RFXtrxEvent) -> None: """Handle received messages from RFXtrx gateway.""" # Log RFXCOM event if not event.device.id_string: @@ -204,7 +206,7 @@ async def async_setup_internal(hass, entry: ConfigEntry): pt2262_devices.append(event.device.id_string) device_entry = device_registry.async_get_device( - identifiers={(DOMAIN, *device_id)}, + identifiers={(DOMAIN, *device_id)}, # type: ignore[arg-type] ) if device_entry: event_data[ATTR_DEVICE_ID] = device_entry.id @@ -216,7 +218,7 @@ async def async_setup_internal(hass, entry: ConfigEntry): hass.bus.async_fire(EVENT_RFXTRX_EVENT, event_data) @callback - def _add_device(event, device_id): + def _add_device(event: rfxtrxmod.RFXtrxEvent, device_id: DeviceTuple) -> None: """Add a device to config entry.""" config = {} config[CONF_DEVICE_ID] = device_id @@ -237,7 +239,7 @@ async def async_setup_internal(hass, entry: ConfigEntry): devices[device_id] = config @callback - def _remove_device(device_id: DeviceTuple): + def _remove_device(device_id: DeviceTuple) -> None: data = { **entry.data, CONF_DEVICES: { @@ -250,7 +252,7 @@ async def async_setup_internal(hass, entry: ConfigEntry): devices.pop(device_id) @callback - def _updated_device(event: Event): + def _updated_device(event: Event) -> None: if event.data["action"] != "remove": return device_entry = device_registry.deleted_devices[event.data["device_id"]] @@ -264,7 +266,7 @@ async def async_setup_internal(hass, entry: ConfigEntry): hass.bus.async_listen(dr.EVENT_DEVICE_REGISTRY_UPDATED, _updated_device) ) - def _shutdown_rfxtrx(event): + def _shutdown_rfxtrx(event: Event) -> None: """Close connection with RFXtrx.""" rfx_object.close_connection() @@ -288,10 +290,15 @@ async def async_setup_platform_entry( async_add_entities: AddEntitiesCallback, supported: Callable[[rfxtrxmod.RFXtrxEvent], bool], constructor: Callable[ - [rfxtrxmod.RFXtrxEvent, rfxtrxmod.RFXtrxEvent | None, DeviceTuple, dict], + [ + rfxtrxmod.RFXtrxEvent, + rfxtrxmod.RFXtrxEvent | None, + DeviceTuple, + dict[str, Any], + ], list[Entity], ], -): +) -> None: """Set up config entry.""" entry_data = config_entry.data device_ids: set[DeviceTuple] = set() @@ -320,7 +327,7 @@ async def async_setup_platform_entry( if entry_data[CONF_AUTOMATIC_ADD]: @callback - def _update(event: rfxtrxmod.RFXtrxEvent, device_id: DeviceTuple): + def _update(event: rfxtrxmod.RFXtrxEvent, device_id: DeviceTuple) -> None: """Handle light updates from the RFXtrx gateway.""" if not supported(event): return @@ -373,7 +380,7 @@ def get_pt2262_cmd(device_id: str, data_bits: int) -> str | None: def get_device_data_bits( - device: rfxtrxmod.RFXtrxDevice, devices: dict[DeviceTuple, dict] + device: rfxtrxmod.RFXtrxDevice, devices: dict[DeviceTuple, dict[str, Any]] ) -> int | None: """Deduce data bits for device based on a cache of device bits.""" data_bits = None @@ -488,9 +495,9 @@ class RfxtrxEntity(RestoreEntity): self._device_id = device_id # If id_string is 213c7f2:1, the group_id is 213c7f2, and the device will respond to # group events regardless of their group indices. - (self._group_id, _, _) = device.id_string.partition(":") + (self._group_id, _, _) = cast(str, device.id_string).partition(":") - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Restore RFXtrx device state (ON/OFF).""" if self._event: self._apply_event(self._event) @@ -500,13 +507,15 @@ class RfxtrxEntity(RestoreEntity): ) @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, str] | None: """Return the device state attributes.""" if not self._event: return None return {ATTR_EVENT: "".join(f"{x:02x}" for x in self._event.data)} - def _event_applies(self, event: rfxtrxmod.RFXtrxEvent, device_id: DeviceTuple): + def _event_applies( + self, event: rfxtrxmod.RFXtrxEvent, device_id: DeviceTuple + ) -> bool: """Check if event applies to me.""" if isinstance(event, rfxtrxmod.ControlEvent): if ( @@ -514,7 +523,7 @@ class RfxtrxEntity(RestoreEntity): and event.values["Command"] in COMMAND_GROUP_LIST ): device: rfxtrxmod.RFXtrxDevice = event.device - (group_id, _, _) = device.id_string.partition(":") + (group_id, _, _) = cast(str, device.id_string).partition(":") return group_id == self._group_id # Otherwise, the event only applies to the matching device. @@ -546,6 +555,6 @@ class RfxtrxCommandEntity(RfxtrxEntity): """Initialzie a switch or light device.""" super().__init__(device, device_id, event=event) - async def _async_send(self, fun, *args): + async def _async_send(self, fun: Callable[..., None], *args: Any) -> None: rfx_object = self.hass.data[DOMAIN][DATA_RFXOBJECT] await self.hass.async_add_executor_job(fun, rfx_object.transport, *args) diff --git a/homeassistant/components/rfxtrx/binary_sensor.py b/homeassistant/components/rfxtrx/binary_sensor.py index 1a0fe698dcf..03ad7ce071b 100644 --- a/homeassistant/components/rfxtrx/binary_sensor.py +++ b/homeassistant/components/rfxtrx/binary_sensor.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any import RFXtrx as rfxtrxmod @@ -14,6 +15,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_COMMAND_OFF, CONF_COMMAND_ON, STATE_ON from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.helpers import event as evt +from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import DeviceTuple, RfxtrxEntity, async_setup_platform_entry, get_pt2262_cmd @@ -72,7 +74,7 @@ SENSOR_TYPES = ( SENSOR_TYPES_DICT = {desc.key: desc for desc in SENSOR_TYPES} -def supported(event: rfxtrxmod.RFXtrxEvent): +def supported(event: rfxtrxmod.RFXtrxEvent) -> bool: """Return whether an event supports binary_sensor.""" if isinstance(event, rfxtrxmod.ControlEvent): return True @@ -91,7 +93,7 @@ async def async_setup_entry( ) -> None: """Set up config entry.""" - def get_sensor_description(type_string: str): + def get_sensor_description(type_string: str) -> BinarySensorEntityDescription: if (description := SENSOR_TYPES_DICT.get(type_string)) is None: return BinarySensorEntityDescription(key=type_string) return description @@ -100,8 +102,8 @@ async def async_setup_entry( event: rfxtrxmod.RFXtrxEvent, auto: rfxtrxmod.RFXtrxEvent | None, device_id: DeviceTuple, - entity_info: dict, - ): + entity_info: dict[str, Any], + ) -> list[Entity]: return [ RfxtrxBinarySensor( @@ -122,10 +124,13 @@ async def async_setup_entry( class RfxtrxBinarySensor(RfxtrxEntity, BinarySensorEntity): - """A representation of a RFXtrx binary sensor.""" + """A representation of a RFXtrx binary sensor. + + Since all repeated events have meaning, these types of sensors + need to have force update enabled. + """ _attr_force_update = True - """We should force updates. Repeated states have meaning.""" def __init__( self, @@ -147,7 +152,7 @@ class RfxtrxBinarySensor(RfxtrxEntity, BinarySensorEntity): self._cmd_on = cmd_on self._cmd_off = cmd_off - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Restore device state.""" await super().async_added_to_hass() @@ -159,7 +164,7 @@ class RfxtrxBinarySensor(RfxtrxEntity, BinarySensorEntity): if self.is_on and self._off_delay is not None: self._attr_is_on = False - def _apply_event_lighting4(self, event: rfxtrxmod.RFXtrxEvent): + def _apply_event_lighting4(self, event: rfxtrxmod.RFXtrxEvent) -> None: """Apply event for a lighting 4 device.""" if self._data_bits is not None: cmdstr = get_pt2262_cmd(event.device.id_string, self._data_bits) @@ -172,7 +177,7 @@ class RfxtrxBinarySensor(RfxtrxEntity, BinarySensorEntity): else: self._attr_is_on = True - def _apply_event_standard(self, event: rfxtrxmod.RFXtrxEvent): + def _apply_event_standard(self, event: rfxtrxmod.RFXtrxEvent) -> None: assert isinstance(event, (rfxtrxmod.SensorEvent, rfxtrxmod.ControlEvent)) if event.values.get("Command") in COMMAND_ON_LIST: self._attr_is_on = True @@ -183,7 +188,7 @@ class RfxtrxBinarySensor(RfxtrxEntity, BinarySensorEntity): elif event.values.get("Sensor Status") in SENSOR_STATUS_OFF: self._attr_is_on = False - def _apply_event(self, event: rfxtrxmod.RFXtrxEvent): + def _apply_event(self, event: rfxtrxmod.RFXtrxEvent) -> None: """Apply command from rfxtrx.""" super()._apply_event(event) if event.device.packettype == DEVICE_PACKET_TYPE_LIGHTING4: @@ -192,7 +197,9 @@ class RfxtrxBinarySensor(RfxtrxEntity, BinarySensorEntity): self._apply_event_standard(event) @callback - def _handle_event(self, event: rfxtrxmod.RFXtrxEvent, device_id: DeviceTuple): + def _handle_event( + self, event: rfxtrxmod.RFXtrxEvent, device_id: DeviceTuple + ) -> None: """Check if event applies to me and update.""" if not self._event_applies(event, device_id): return @@ -215,7 +222,7 @@ class RfxtrxBinarySensor(RfxtrxEntity, BinarySensorEntity): if self.is_on and self._off_delay is not None: @callback - def off_delay_listener(now): + def off_delay_listener(now: Any) -> None: """Switch device off after a delay.""" self._delay_listener = None self._attr_is_on = False diff --git a/homeassistant/components/rfxtrx/config_flow.py b/homeassistant/components/rfxtrx/config_flow.py index 508ca9a7037..2aa3bd20b8c 100644 --- a/homeassistant/components/rfxtrx/config_flow.py +++ b/homeassistant/components/rfxtrx/config_flow.py @@ -1,17 +1,18 @@ """Config flow for RFXCOM RFXtrx integration.""" from __future__ import annotations +import asyncio import copy import itertools import os -from typing import TypedDict, cast +from typing import Any, TypedDict, cast import RFXtrx as rfxtrxmod import serial import serial.tools.list_ports import voluptuous as vol -from homeassistant import config_entries, exceptions +from homeassistant import config_entries, data_entry_flow, exceptions from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_COMMAND_OFF, @@ -23,12 +24,13 @@ from homeassistant.const import ( CONF_PORT, CONF_TYPE, ) -from homeassistant.core import callback +from homeassistant.core import State, callback from homeassistant.helpers import ( config_validation as cv, device_registry as dr, entity_registry as er, ) +from homeassistant.helpers.event import async_track_state_change from . import ( DOMAIN, @@ -64,7 +66,7 @@ class DeviceData(TypedDict): device_id: DeviceTuple -def none_or_int(value, base): +def none_or_int(value: str | None, base: int) -> int | None: """Check if strin is one otherwise convert to int.""" if value is None: return None @@ -80,17 +82,21 @@ class OptionsFlow(config_entries.OptionsFlow): def __init__(self, config_entry: ConfigEntry) -> None: """Initialize rfxtrx options flow.""" self._config_entry = config_entry - self._global_options = None - self._selected_device = None + self._global_options: dict[str, Any] = {} + self._selected_device: dict[str, Any] = {} self._selected_device_entry_id: str | None = None self._selected_device_event_code: str | None = None self._selected_device_object: rfxtrxmod.RFXtrxEvent | None = None - async def async_step_init(self, user_input=None): + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> data_entry_flow.FlowResult: """Manage the options.""" return await self.async_step_prompt_options() - async def async_step_prompt_options(self, user_input=None): + async def async_step_prompt_options( + self, user_input: dict[str, Any] | None = None + ) -> data_entry_flow.FlowResult: """Prompt for options.""" errors = {} @@ -103,7 +109,8 @@ class OptionsFlow(config_entries.OptionsFlow): entry_id = user_input[CONF_DEVICE] device_data = self._get_device_data(entry_id) self._selected_device_entry_id = entry_id - event_code = device_data[CONF_EVENT_CODE] + event_code = device_data["event_code"] + assert event_code self._selected_device_event_code = event_code self._selected_device = self._config_entry.data[CONF_DEVICES][ event_code @@ -111,7 +118,9 @@ class OptionsFlow(config_entries.OptionsFlow): self._selected_device_object = get_rfx_object(event_code) return await self.async_step_set_device_options() if CONF_EVENT_CODE in user_input: - self._selected_device_event_code = user_input[CONF_EVENT_CODE] + self._selected_device_event_code = cast( + str, user_input[CONF_EVENT_CODE] + ) self._selected_device = {} selected_device_object = get_rfx_object( self._selected_device_event_code @@ -159,13 +168,17 @@ class OptionsFlow(config_entries.OptionsFlow): step_id="prompt_options", data_schema=vol.Schema(options), errors=errors ) - async def async_step_set_device_options(self, user_input=None): + async def async_step_set_device_options( + self, user_input: dict[str, Any] | None = None + ) -> data_entry_flow.FlowResult: """Manage device options.""" errors = {} + assert self._selected_device_object + assert self._selected_device_event_code if user_input is not None: - assert self._selected_device_object - assert self._selected_device_event_code + devices: dict[str, dict[str, Any] | None] = {} + device: dict[str, Any] device_id = get_device_id( self._selected_device_object.device, data_bits=user_input.get(CONF_DATA_BITS), @@ -278,16 +291,16 @@ class OptionsFlow(config_entries.OptionsFlow): ), } ) - devices = { + replace_devices = { entry.id: entry.name_by_user if entry.name_by_user else entry.name for entry in self._device_entries if self._can_replace_device(entry.id) } - if devices: + if replace_devices: data_schema.update( { - vol.Optional(CONF_REPLACE_DEVICE): vol.In(devices), + vol.Optional(CONF_REPLACE_DEVICE): vol.In(replace_devices), } ) @@ -297,11 +310,13 @@ class OptionsFlow(config_entries.OptionsFlow): errors=errors, ) - async def _async_replace_device(self, replace_device): + async def _async_replace_device(self, replace_device: str) -> None: """Migrate properties of a device into another.""" device_registry = self._device_registry old_device = self._selected_device_entry_id + assert old_device old_entry = device_registry.async_get(old_device) + assert old_entry device_registry.async_update_device( replace_device, area_id=old_entry.area_id, @@ -330,9 +345,35 @@ class OptionsFlow(config_entries.OptionsFlow): if new_entity_id is not None: entity_migration_map[new_entity_id] = entry + @callback + def _handle_state_change( + entity_id: str, old_state: State | None, new_state: State | None + ) -> None: + # Wait for entities to finish cleanup + if new_state is None and entity_id in pending_entities: + pending_entities.remove(entity_id) + if not pending_entities: + wait_for_entities.set() + + # Create a set with entities to be removed which are currently in the state + # machine + pending_entities = { + entry.entity_id + for entry in entity_migration_map.values() + if not self.hass.states.async_available(entry.entity_id) + } + wait_for_entities = asyncio.Event() + remove_track_state_changes = async_track_state_change( + self.hass, pending_entities, _handle_state_change + ) + for entry in entity_migration_map.values(): entity_registry.async_remove(entry.entity_id) + # Wait for entities to finish cleanup + await wait_for_entities.wait() + remove_track_state_changes() + for entity_id, entry in entity_migration_map.items(): entity_registry.async_update_entity( entity_id, @@ -343,23 +384,29 @@ class OptionsFlow(config_entries.OptionsFlow): device_registry.async_remove_device(old_device) - def _can_add_device(self, new_rfx_obj): + def _can_add_device(self, new_rfx_obj: rfxtrxmod.RFXtrxEvent) -> bool: """Check if device does not already exist.""" new_device_id = get_device_id(new_rfx_obj.device) for packet_id, entity_info in self._config_entry.data[CONF_DEVICES].items(): rfx_obj = get_rfx_object(packet_id) + assert rfx_obj + device_id = get_device_id(rfx_obj.device, entity_info.get(CONF_DATA_BITS)) if new_device_id == device_id: return False return True - def _can_replace_device(self, entry_id): + def _can_replace_device(self, entry_id: str) -> bool: """Check if device can be replaced with selected device.""" + assert self._selected_device_object + device_data = self._get_device_data(entry_id) - if (event_code := device_data[CONF_EVENT_CODE]) is not None: + if (event_code := device_data["event_code"]) is not None: rfx_obj = get_rfx_object(event_code) + assert rfx_obj + if ( rfx_obj.device.packettype == self._selected_device_object.device.packettype @@ -371,12 +418,12 @@ class OptionsFlow(config_entries.OptionsFlow): return False - def _get_device_event_code(self, entry_id): + def _get_device_event_code(self, entry_id: str) -> str | None: data = self._get_device_data(entry_id) - return data[CONF_EVENT_CODE] + return data["event_code"] - def _get_device_data(self, entry_id) -> DeviceData: + def _get_device_data(self, entry_id: str) -> DeviceData: """Get event code based on device identifier.""" event_code: str | None = None entry = self._device_registry.async_get(entry_id) @@ -390,7 +437,11 @@ class OptionsFlow(config_entries.OptionsFlow): return DeviceData(event_code=event_code, device_id=device_id) @callback - def update_config_data(self, global_options=None, devices=None): + def update_config_data( + self, + global_options: dict[str, Any] | None = None, + devices: dict[str, Any] | None = None, + ) -> None: """Update data in ConfigEntry.""" entry_data = self._config_entry.data.copy() entry_data[CONF_DEVICES] = copy.deepcopy(self._config_entry.data[CONF_DEVICES]) @@ -413,12 +464,14 @@ 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 + ) -> data_entry_flow.FlowResult: """Step when user initializes a integration.""" await self.async_set_unique_id(DOMAIN) self._abort_if_unique_id_configured() - errors = {} + errors: dict[str, str] = {} if user_input is not None: if user_input[CONF_TYPE] == "Serial": return await self.async_step_setup_serial() @@ -430,9 +483,11 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): schema = vol.Schema({vol.Required(CONF_TYPE): vol.In(list_of_types)}) return self.async_show_form(step_id="user", data_schema=schema, errors=errors) - async def async_step_setup_network(self, user_input=None): + async def async_step_setup_network( + self, user_input: dict[str, Any] | None = None + ) -> data_entry_flow.FlowResult: """Step when setting up network configuration.""" - errors = {} + errors: dict[str, str] = {} if user_input is not None: host = user_input[CONF_HOST] @@ -455,9 +510,11 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_setup_serial(self, user_input=None): + async def async_step_setup_serial( + self, user_input: dict[str, Any] | None = None + ) -> data_entry_flow.FlowResult: """Step when setting up serial configuration.""" - errors = {} + errors: dict[str, str] = {} if user_input is not None: user_selection = user_input[CONF_DEVICE] @@ -493,9 +550,11 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_setup_serial_manual_path(self, user_input=None): + async def async_step_setup_serial_manual_path( + self, user_input: dict[str, Any] | None = None + ) -> data_entry_flow.FlowResult: """Select path manually.""" - errors = {} + errors: dict[str, str] = {} if user_input is not None: device = user_input[CONF_DEVICE] @@ -514,7 +573,12 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_validate_rfx(self, host=None, port=None, device=None): + async def async_validate_rfx( + self, + host: str | None = None, + port: int | None = None, + device: str | None = None, + ) -> dict[str, Any]: """Create data for rfxtrx entry.""" success = await self.hass.async_add_executor_job( _test_transport, host, port, device @@ -522,7 +586,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if not success: raise CannotConnect - data = { + data: dict[str, Any] = { CONF_HOST: host, CONF_PORT: port, CONF_DEVICE: device, @@ -538,7 +602,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return OptionsFlow(config_entry) -def _test_transport(host, port, device): +def _test_transport(host: str | None, port: int | None, device: str | None) -> bool: """Construct a rfx object based on config.""" if port is not None: try: diff --git a/homeassistant/components/rfxtrx/cover.py b/homeassistant/components/rfxtrx/cover.py index 5e1788194f5..532e41ac50c 100644 --- a/homeassistant/components/rfxtrx/cover.py +++ b/homeassistant/components/rfxtrx/cover.py @@ -10,6 +10,7 @@ from homeassistant.components.cover import CoverEntity, CoverEntityFeature from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_OPEN from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import DeviceTuple, RfxtrxCommandEntity, async_setup_platform_entry @@ -24,9 +25,9 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -def supported(event: rfxtrxmod.RFXtrxEvent): +def supported(event: rfxtrxmod.RFXtrxEvent) -> bool: """Return whether an event supports cover.""" - return event.device.known_to_be_rollershutter + return bool(event.device.known_to_be_rollershutter) async def async_setup_entry( @@ -40,8 +41,8 @@ async def async_setup_entry( event: rfxtrxmod.RFXtrxEvent, auto: rfxtrxmod.RFXtrxEvent | None, device_id: DeviceTuple, - entity_info: dict, - ): + entity_info: dict[str, Any], + ) -> list[Entity]: return [ RfxtrxCover( event.device, @@ -144,7 +145,7 @@ class RfxtrxCover(RfxtrxCommandEntity, CoverEntity): self._attr_is_closed = False self.async_write_ha_state() - def _apply_event(self, event: rfxtrxmod.RFXtrxEvent): + def _apply_event(self, event: rfxtrxmod.RFXtrxEvent) -> None: """Apply command from rfxtrx.""" assert isinstance(event, rfxtrxmod.ControlEvent) super()._apply_event(event) @@ -154,7 +155,9 @@ class RfxtrxCover(RfxtrxCommandEntity, CoverEntity): self._attr_is_closed = True @callback - def _handle_event(self, event: rfxtrxmod.RFXtrxEvent, device_id: DeviceTuple): + def _handle_event( + self, event: rfxtrxmod.RFXtrxEvent, device_id: DeviceTuple + ) -> None: """Check if event applies to me and update.""" if device_id != self._device_id: return diff --git a/homeassistant/components/rfxtrx/device_action.py b/homeassistant/components/rfxtrx/device_action.py index e8fc8c6707d..7ea4ed07423 100644 --- a/homeassistant/components/rfxtrx/device_action.py +++ b/homeassistant/components/rfxtrx/device_action.py @@ -1,6 +1,8 @@ """Provides device automations for RFXCOM RFXtrx.""" from __future__ import annotations +from collections.abc import Callable + import voluptuous as vol from homeassistant.components.device_automation.exceptions import ( @@ -65,7 +67,9 @@ async def async_get_actions( return actions -def _get_commands(hass, device_id, action_type): +def _get_commands( + hass: HomeAssistant, device_id: str, action_type: str +) -> tuple[dict[str, str], Callable[..., None]]: device = async_get_device_object(hass, device_id) send_fun = getattr(device, action_type) commands = getattr(device, ACTION_SELECTION[action_type], {}) @@ -76,7 +80,6 @@ async def async_validate_action_config( hass: HomeAssistant, config: ConfigType ) -> ConfigType: """Validate config.""" - config = ACTION_SCHEMA(config) commands, _ = _get_commands(hass, config[CONF_DEVICE_ID], config[CONF_TYPE]) sub_type = config[CONF_SUBTYPE] diff --git a/homeassistant/components/rfxtrx/helpers.py b/homeassistant/components/rfxtrx/helpers.py index 7e567cff1cd..2badc6d4ca5 100644 --- a/homeassistant/components/rfxtrx/helpers.py +++ b/homeassistant/components/rfxtrx/helpers.py @@ -1,14 +1,14 @@ """Provides helpers for RFXtrx.""" -from RFXtrx import get_device +from RFXtrx import RFXtrxDevice, get_device from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry as dr @callback -def async_get_device_object(hass: HomeAssistant, device_id): +def async_get_device_object(hass: HomeAssistant, device_id: str) -> RFXtrxDevice: """Get a device for the given device registry id.""" device_registry = dr.async_get(hass) registry_device = device_registry.async_get(device_id) diff --git a/homeassistant/components/rfxtrx/light.py b/homeassistant/components/rfxtrx/light.py index 7f32ee0bc83..ad84515d41d 100644 --- a/homeassistant/components/rfxtrx/light.py +++ b/homeassistant/components/rfxtrx/light.py @@ -10,6 +10,7 @@ from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEnti from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_ON from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import DeviceTuple, RfxtrxCommandEntity, async_setup_platform_entry @@ -18,7 +19,7 @@ from .const import COMMAND_OFF_LIST, COMMAND_ON_LIST _LOGGER = logging.getLogger(__name__) -def supported(event: rfxtrxmod.RFXtrxEvent): +def supported(event: rfxtrxmod.RFXtrxEvent) -> bool: """Return whether an event supports light.""" return ( isinstance(event.device, rfxtrxmod.LightingDevice) @@ -37,8 +38,8 @@ async def async_setup_entry( event: rfxtrxmod.RFXtrxEvent, auto: rfxtrxmod.RFXtrxEvent | None, device_id: DeviceTuple, - entity_info: dict, - ): + entity_info: dict[str, Any], + ) -> list[Entity]: return [ RfxtrxLight( event.device, @@ -91,7 +92,7 @@ class RfxtrxLight(RfxtrxCommandEntity, LightEntity): self._attr_brightness = 0 self.async_write_ha_state() - def _apply_event(self, event: rfxtrxmod.RFXtrxEvent): + def _apply_event(self, event: rfxtrxmod.RFXtrxEvent) -> None: """Apply command from rfxtrx.""" assert isinstance(event, rfxtrxmod.ControlEvent) super()._apply_event(event) @@ -105,7 +106,9 @@ class RfxtrxLight(RfxtrxCommandEntity, LightEntity): self._attr_is_on = brightness > 0 @callback - def _handle_event(self, event, device_id): + def _handle_event( + self, event: rfxtrxmod.RFXtrxEvent, device_id: DeviceTuple + ) -> None: """Check if event applies to me and update.""" if device_id != self._device_id: return diff --git a/homeassistant/components/rfxtrx/sensor.py b/homeassistant/components/rfxtrx/sensor.py index 86c3eabc922..563b166e0aa 100644 --- a/homeassistant/components/rfxtrx/sensor.py +++ b/homeassistant/components/rfxtrx/sensor.py @@ -3,9 +3,12 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass +from datetime import date, datetime +from decimal import Decimal import logging +from typing import Any, cast -from RFXtrx import ControlEvent, RFXtrxEvent, SensorEvent +from RFXtrx import ControlEvent, RFXtrxDevice, RFXtrxEvent, SensorEvent from homeassistant.components.sensor import ( SensorDeviceClass, @@ -30,8 +33,9 @@ from homeassistant.const import ( UV_INDEX, ) from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity import Entity, EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType from . import DeviceTuple, RfxtrxEntity, async_setup_platform_entry, get_rfx_object from .const import ATTR_EVENT @@ -39,14 +43,14 @@ from .const import ATTR_EVENT _LOGGER = logging.getLogger(__name__) -def _battery_convert(value): +def _battery_convert(value: int | None) -> int | None: """Battery is given as a value between 0 and 9.""" if value is None: return None return (value + 1) * 10 -def _rssi_convert(value): +def _rssi_convert(value: int | None) -> str | None: """Rssi is given as dBm value.""" if value is None: return None @@ -57,7 +61,9 @@ def _rssi_convert(value): class RfxtrxSensorEntityDescription(SensorEntityDescription): """Description of sensor entities.""" - convert: Callable = lambda x: x + convert: Callable[[Any], StateType | date | datetime | Decimal] = lambda x: cast( + StateType, x + ) SENSOR_TYPES = ( @@ -200,12 +206,14 @@ SENSOR_TYPES = ( name="Wind average speed", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=SPEED_METERS_PER_SECOND, + device_class=SensorDeviceClass.SPEED, ), RfxtrxSensorEntityDescription( key="Wind gust", name="Wind gust", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=SPEED_METERS_PER_SECOND, + device_class=SensorDeviceClass.SPEED, ), RfxtrxSensorEntityDescription( key="Rain total", @@ -243,16 +251,16 @@ async def async_setup_entry( ) -> None: """Set up config entry.""" - def _supported(event): + def _supported(event: RFXtrxEvent) -> bool: return isinstance(event, (ControlEvent, SensorEvent)) def _constructor( event: RFXtrxEvent, auto: RFXtrxEvent | None, device_id: DeviceTuple, - entity_info: dict, - ): - entities: list[RfxtrxSensor] = [] + entity_info: dict[str, Any], + ) -> list[Entity]: + entities: list[Entity] = [] for data_type in set(event.values) & set(SENSOR_TYPES_DICT): entities.append( RfxtrxSensor( @@ -271,20 +279,28 @@ async def async_setup_entry( class RfxtrxSensor(RfxtrxEntity, SensorEntity): - """Representation of a RFXtrx sensor.""" + """Representation of a RFXtrx sensor. + + Since all repeated events have meaning, these types of sensors + need to have force update enabled. + """ _attr_force_update = True - """We should force updates. Repeated states have meaning.""" - entity_description: RfxtrxSensorEntityDescription - def __init__(self, device, device_id, entity_description, event=None): + def __init__( + self, + device: RFXtrxDevice, + device_id: DeviceTuple, + entity_description: RfxtrxSensorEntityDescription, + event: RFXtrxEvent | None = None, + ) -> None: """Initialize the sensor.""" super().__init__(device, device_id, event=event) self.entity_description = entity_description self._attr_unique_id = "_".join(x for x in (*device_id, entity_description.key)) - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Restore device state.""" await super().async_added_to_hass() @@ -296,7 +312,7 @@ class RfxtrxSensor(RfxtrxEntity, SensorEntity): self._apply_event(get_rfx_object(event)) @property - def native_value(self): + def native_value(self) -> StateType | date | datetime | Decimal: """Return the state of the sensor.""" if not self._event: return None @@ -304,7 +320,7 @@ class RfxtrxSensor(RfxtrxEntity, SensorEntity): return self.entity_description.convert(value) @callback - def _handle_event(self, event, device_id): + def _handle_event(self, event: RFXtrxEvent, device_id: DeviceTuple) -> None: """Check if event applies to me and update.""" if device_id != self._device_id: return diff --git a/homeassistant/components/rfxtrx/siren.py b/homeassistant/components/rfxtrx/siren.py index acf06518959..c9f10febb6b 100644 --- a/homeassistant/components/rfxtrx/siren.py +++ b/homeassistant/components/rfxtrx/siren.py @@ -6,8 +6,7 @@ from typing import Any import RFXtrx as rfxtrxmod -from homeassistant.components.siren import SirenEntity, SirenEntityFeature -from homeassistant.components.siren.const import ATTR_TONE +from homeassistant.components.siren import ATTR_TONE, SirenEntity, SirenEntityFeature from homeassistant.config_entries import ConfigEntry from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.helpers.entity import Entity @@ -59,8 +58,8 @@ async def async_setup_entry( event: rfxtrxmod.RFXtrxEvent, auto: rfxtrxmod.RFXtrxEvent | None, device_id: DeviceTuple, - entity_info: dict, - ): + entity_info: dict[str, Any], + ) -> list[Entity]: """Construct a entity from an event.""" device = event.device @@ -86,6 +85,7 @@ async def async_setup_entry( auto, ) ] + return [] await async_setup_platform_entry( hass, config_entry, async_add_entities, supported, _constructor diff --git a/homeassistant/components/rfxtrx/switch.py b/homeassistant/components/rfxtrx/switch.py index f164e54b212..edc34aeb80d 100644 --- a/homeassistant/components/rfxtrx/switch.py +++ b/homeassistant/components/rfxtrx/switch.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any import RFXtrx as rfxtrxmod @@ -9,6 +10,7 @@ from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_COMMAND_OFF, CONF_COMMAND_ON, STATE_ON from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import ( @@ -30,7 +32,7 @@ DATA_SWITCH = f"{DOMAIN}_switch" _LOGGER = logging.getLogger(__name__) -def supported(event): +def supported(event: rfxtrxmod.RFXtrxEvent) -> bool: """Return whether an event supports switch.""" return ( isinstance(event.device, rfxtrxmod.LightingDevice) @@ -51,8 +53,8 @@ async def async_setup_entry( event: rfxtrxmod.RFXtrxEvent, auto: rfxtrxmod.RFXtrxEvent | None, device_id: DeviceTuple, - entity_info: dict, - ): + entity_info: dict[str, Any], + ) -> list[Entity]: return [ RfxtrxSwitch( event.device, @@ -87,7 +89,7 @@ class RfxtrxSwitch(RfxtrxCommandEntity, SwitchEntity): self._cmd_on = cmd_on self._cmd_off = cmd_off - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Restore device state.""" await super().async_added_to_hass() @@ -96,7 +98,7 @@ class RfxtrxSwitch(RfxtrxCommandEntity, SwitchEntity): if old_state is not None: self._attr_is_on = old_state.state == STATE_ON - def _apply_event_lighting4(self, event: rfxtrxmod.RFXtrxEvent): + def _apply_event_lighting4(self, event: rfxtrxmod.RFXtrxEvent) -> None: """Apply event for a lighting 4 device.""" if self._data_bits is not None: cmdstr = get_pt2262_cmd(event.device.id_string, self._data_bits) @@ -134,7 +136,7 @@ class RfxtrxSwitch(RfxtrxCommandEntity, SwitchEntity): self.async_write_ha_state() - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" if self._cmd_on is not None: await self._async_send(self._device.send_command, self._cmd_on) @@ -143,7 +145,7 @@ class RfxtrxSwitch(RfxtrxCommandEntity, SwitchEntity): self._attr_is_on = True self.async_write_ha_state() - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" if self._cmd_off is not None: await self._async_send(self._device.send_command, self._cmd_off) diff --git a/homeassistant/components/rfxtrx/translations/hu.json b/homeassistant/components/rfxtrx/translations/hu.json index ebdde5c9428..10a659ff3c2 100644 --- a/homeassistant/components/rfxtrx/translations/hu.json +++ b/homeassistant/components/rfxtrx/translations/hu.json @@ -78,7 +78,7 @@ "off_delay": "Kikapcsol\u00e1si k\u00e9sleltet\u00e9s", "off_delay_enabled": "Kikapcsol\u00e1si k\u00e9sleltet\u00e9s enged\u00e9lyez\u00e9se", "replace_device": "V\u00e1lassza ki a cser\u00e9lni k\u00edv\u00e1nt eszk\u00f6zt", - "venetian_blind_mode": "Velencei red\u0151ny \u00fczemm\u00f3d" + "venetian_blind_mode": "Reluxa m\u00f3d" }, "title": "Konfigur\u00e1lja az eszk\u00f6z be\u00e1ll\u00edt\u00e1sait" } diff --git a/homeassistant/components/rhasspy/translations/bg.json b/homeassistant/components/rhasspy/translations/bg.json new file mode 100644 index 00000000000..1c6120581b0 --- /dev/null +++ b/homeassistant/components/rhasspy/translations/bg.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "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." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rhasspy/translations/cs.json b/homeassistant/components/rhasspy/translations/cs.json new file mode 100644 index 00000000000..edf19d3cdb8 --- /dev/null +++ b/homeassistant/components/rhasspy/translations/cs.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "description": "Chcete povolit podporu Rhasspy?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ridwell/translations/cs.json b/homeassistant/components/ridwell/translations/cs.json index 5d43feee500..8af205757ee 100644 --- a/homeassistant/components/ridwell/translations/cs.json +++ b/homeassistant/components/ridwell/translations/cs.json @@ -7,6 +7,20 @@ "error": { "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Heslo" + }, + "title": "Znovu ov\u011b\u0159it integraci" + }, + "user": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/ridwell/translations/es.json b/homeassistant/components/ridwell/translations/es.json index 07037e942cf..61ba7c53361 100644 --- a/homeassistant/components/ridwell/translations/es.json +++ b/homeassistant/components/ridwell/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "error": { "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", diff --git a/homeassistant/components/ring/binary_sensor.py b/homeassistant/components/ring/binary_sensor.py index efa92475551..fc47ad7cbf0 100644 --- a/homeassistant/components/ring/binary_sensor.py +++ b/homeassistant/components/ring/binary_sensor.py @@ -88,13 +88,13 @@ class RingBinarySensor(RingEntityMixin, BinarySensorEntity): self._attr_unique_id = f"{device.id}-{description.key}" self._update_alert() - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register callbacks.""" await super().async_added_to_hass() self.ring_objects["dings_data"].async_add_listener(self._dings_update_callback) self._dings_update_callback() - async def async_will_remove_from_hass(self): + async def async_will_remove_from_hass(self) -> None: """Disconnect callbacks.""" await super().async_will_remove_from_hass() self.ring_objects["dings_data"].async_remove_listener( diff --git a/homeassistant/components/ring/camera.py b/homeassistant/components/ring/camera.py index 5bf440cfcd9..f5d70a86cb3 100644 --- a/homeassistant/components/ring/camera.py +++ b/homeassistant/components/ring/camera.py @@ -60,7 +60,7 @@ class RingCam(RingEntityMixin, Camera): self._image = None self._expires_at = dt_util.utcnow() - FORCE_REFRESH_INTERVAL - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register callbacks.""" await super().async_added_to_hass() @@ -68,7 +68,7 @@ class RingCam(RingEntityMixin, Camera): self._device, self._history_update_callback ) - async def async_will_remove_from_hass(self): + async def async_will_remove_from_hass(self) -> None: """Disconnect callbacks.""" await super().async_will_remove_from_hass() @@ -144,7 +144,7 @@ class RingCam(RingEntityMixin, Camera): finally: await stream.close() - async def async_update(self): + async def async_update(self) -> None: """Update camera entity and refresh attributes.""" if self._last_event is None: return diff --git a/homeassistant/components/ring/sensor.py b/homeassistant/components/ring/sensor.py index 26068c149ce..1aaa073064f 100644 --- a/homeassistant/components/ring/sensor.py +++ b/homeassistant/components/ring/sensor.py @@ -82,7 +82,7 @@ class RingSensor(RingEntityMixin, SensorEntity): class HealthDataRingSensor(RingSensor): """Ring sensor that relies on health data.""" - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register callbacks.""" await super().async_added_to_hass() @@ -90,7 +90,7 @@ class HealthDataRingSensor(RingSensor): self._device, self._health_update_callback ) - async def async_will_remove_from_hass(self): + async def async_will_remove_from_hass(self) -> None: """Disconnect callbacks.""" await super().async_will_remove_from_hass() @@ -125,7 +125,7 @@ class HistoryRingSensor(RingSensor): _latest_event = None - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register callbacks.""" await super().async_added_to_hass() @@ -133,7 +133,7 @@ class HistoryRingSensor(RingSensor): self._device, self._history_update_callback ) - async def async_will_remove_from_hass(self): + async def async_will_remove_from_hass(self) -> None: """Disconnect callbacks.""" await super().async_will_remove_from_hass() diff --git a/homeassistant/components/ring/switch.py b/homeassistant/components/ring/switch.py index 682404e57f9..0fa6e3b1114 100644 --- a/homeassistant/components/ring/switch.py +++ b/homeassistant/components/ring/switch.py @@ -1,6 +1,7 @@ """This component provides HA switch support for Ring Door Bell/Chimes.""" from datetime import timedelta import logging +from typing import Any import requests @@ -97,11 +98,11 @@ class SirenSwitch(BaseRingSwitch): """If the switch is currently on or off.""" return self._siren_on - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the siren on for 30 seconds.""" self._set_switch(1) - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the siren off.""" self._set_switch(0) diff --git a/homeassistant/components/ripple/sensor.py b/homeassistant/components/ripple/sensor.py index cbb72ab8e57..01f326b2a63 100644 --- a/homeassistant/components/ripple/sensor.py +++ b/homeassistant/components/ripple/sensor.py @@ -70,7 +70,7 @@ class RippleSensor(SensorEntity): """Return the state attributes of the sensor.""" return {ATTR_ATTRIBUTION: ATTRIBUTION} - def update(self): + def update(self) -> None: """Get the latest state of the sensor.""" if (balance := get_balance(self.address)) is not None: self._state = balance diff --git a/homeassistant/components/risco/translations/bg.json b/homeassistant/components/risco/translations/bg.json index 805d72102aa..5e165a9dcfe 100644 --- a/homeassistant/components/risco/translations/bg.json +++ b/homeassistant/components/risco/translations/bg.json @@ -9,11 +9,29 @@ "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "step": { + "cloud": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "pin": "\u041f\u0418\u041d \u043a\u043e\u0434", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + } + }, + "local": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "pin": "\u041f\u0418\u041d \u043a\u043e\u0434", + "port": "\u041f\u043e\u0440\u0442" + } + }, "user": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u0430", "pin": "\u041f\u0418\u041d \u043a\u043e\u0434", "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + }, + "menu_options": { + "cloud": "Risco Cloud (\u043f\u0440\u0435\u043f\u043e\u0440\u044a\u0447\u0438\u0442\u0435\u043b\u043d\u043e)", + "local": "\u041b\u043e\u043a\u0430\u043b\u0435\u043d \u043f\u0430\u043d\u0435\u043b Risco (\u0437\u0430 \u043d\u0430\u043f\u0440\u0435\u0434\u043d\u0430\u043b\u0438)" } } } diff --git a/homeassistant/components/risco/translations/cs.json b/homeassistant/components/risco/translations/cs.json index a4d2971e220..8698052c0fa 100644 --- a/homeassistant/components/risco/translations/cs.json +++ b/homeassistant/components/risco/translations/cs.json @@ -9,6 +9,20 @@ "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, "step": { + "cloud": { + "data": { + "password": "Heslo", + "pin": "PIN k\u00f3d", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + }, + "local": { + "data": { + "host": "Hostitel", + "pin": "PIN k\u00f3d", + "port": "Port" + } + }, "user": { "data": { "password": "Heslo", diff --git a/homeassistant/components/risco/translations/nl.json b/homeassistant/components/risco/translations/nl.json index 9b7bb7a43c8..db2a4996c77 100644 --- a/homeassistant/components/risco/translations/nl.json +++ b/homeassistant/components/risco/translations/nl.json @@ -16,6 +16,13 @@ "username": "Gebruikersnaam" } }, + "local": { + "data": { + "host": "Host", + "pin": "Pincode", + "port": "Poort" + } + }, "user": { "data": { "password": "Wachtwoord", diff --git a/homeassistant/components/risco/translations/pl.json b/homeassistant/components/risco/translations/pl.json index e5523667231..0e4ea5302d7 100644 --- a/homeassistant/components/risco/translations/pl.json +++ b/homeassistant/components/risco/translations/pl.json @@ -28,6 +28,10 @@ "password": "Has\u0142o", "pin": "Kod PIN", "username": "Nazwa u\u017cytkownika" + }, + "menu_options": { + "cloud": "Chmura Risco (zalecane)", + "local": "Lokalny Panel Risco (zaawansowane)" } } } diff --git a/homeassistant/components/risco/translations/pt.json b/homeassistant/components/risco/translations/pt.json index 7b98c6234c5..30b87b0a0da 100644 --- a/homeassistant/components/risco/translations/pt.json +++ b/homeassistant/components/risco/translations/pt.json @@ -14,6 +14,10 @@ "password": "Palavra-passe", "pin": "C\u00f3digo PIN", "username": "Nome de Utilizador" + }, + "menu_options": { + "cloud": "Risco Cloud (recomendado)", + "local": "Painel de Risco Local (avan\u00e7ado)" } } } diff --git a/homeassistant/components/risco/translations/sv.json b/homeassistant/components/risco/translations/sv.json index f0194b4edf5..dd89d9c43f7 100644 --- a/homeassistant/components/risco/translations/sv.json +++ b/homeassistant/components/risco/translations/sv.json @@ -9,11 +9,29 @@ "unknown": "Ov\u00e4ntat fel" }, "step": { + "cloud": { + "data": { + "password": "L\u00f6senord", + "pin": "Pin-kod", + "username": "Anv\u00e4ndarnamn" + } + }, + "local": { + "data": { + "host": "V\u00e4rd", + "pin": "Pin-kod", + "port": "Port" + } + }, "user": { "data": { "password": "L\u00f6senord", "pin": "Pin-kod", "username": "Anv\u00e4ndarnamn" + }, + "menu_options": { + "cloud": "Risco Cloud (rekommenderas)", + "local": "Lokal Risco Panel (avancerat)" } } } diff --git a/homeassistant/components/risco/translations/tr.json b/homeassistant/components/risco/translations/tr.json index 168fc621e05..6572e50de4f 100644 --- a/homeassistant/components/risco/translations/tr.json +++ b/homeassistant/components/risco/translations/tr.json @@ -9,11 +9,29 @@ "unknown": "Beklenmeyen hata" }, "step": { + "cloud": { + "data": { + "password": "Parola", + "pin": "PIN Kodu", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + }, + "local": { + "data": { + "host": "Sunucu", + "pin": "PIN Kodu", + "port": "Port" + } + }, "user": { "data": { "password": "Parola", "pin": "PIN Kodu", "username": "Kullan\u0131c\u0131 Ad\u0131" + }, + "menu_options": { + "cloud": "Risco Cloud (\u00f6nerilir)", + "local": "Yerel Risco Paneli (geli\u015fmi\u015f)" } } } diff --git a/homeassistant/components/rmvtransport/sensor.py b/homeassistant/components/rmvtransport/sensor.py index 3394c4ef4d3..25f14ae6df3 100644 --- a/homeassistant/components/rmvtransport/sensor.py +++ b/homeassistant/components/rmvtransport/sensor.py @@ -186,7 +186,7 @@ class RMVDepartureSensor(SensorEntity): """Return the unit this state is expressed in.""" return TIME_MINUTES - async def async_update(self): + async def async_update(self) -> None: """Get the latest data and update the state.""" await self.data.async_update() diff --git a/homeassistant/components/roku/browse_media.py b/homeassistant/components/roku/browse_media.py index 72b572e8d3e..07e65ed3891 100644 --- a/homeassistant/components/roku/browse_media.py +++ b/homeassistant/components/roku/browse_media.py @@ -3,20 +3,14 @@ from __future__ import annotations from collections.abc import Callable from functools import partial -from urllib.parse import quote_plus from homeassistant.components import media_source -from homeassistant.components.media_player import BrowseMedia -from homeassistant.components.media_player.const import ( - MEDIA_CLASS_APP, - MEDIA_CLASS_CHANNEL, - MEDIA_CLASS_DIRECTORY, - MEDIA_TYPE_APP, - MEDIA_TYPE_APPS, - MEDIA_TYPE_CHANNEL, - MEDIA_TYPE_CHANNELS, +from homeassistant.components.media_player import ( + BrowseError, + BrowseMedia, + MediaClass, + MediaType, ) -from homeassistant.components.media_player.errors import BrowseError from homeassistant.core import HomeAssistant from homeassistant.helpers.network import is_internal_request @@ -24,25 +18,25 @@ from .coordinator import RokuDataUpdateCoordinator from .helpers import format_channel_name CONTENT_TYPE_MEDIA_CLASS = { - MEDIA_TYPE_APP: MEDIA_CLASS_APP, - MEDIA_TYPE_APPS: MEDIA_CLASS_APP, - MEDIA_TYPE_CHANNEL: MEDIA_CLASS_CHANNEL, - MEDIA_TYPE_CHANNELS: MEDIA_CLASS_CHANNEL, + MediaType.APP: MediaClass.APP, + MediaType.APPS: MediaClass.APP, + MediaType.CHANNEL: MediaClass.CHANNEL, + MediaType.CHANNELS: MediaClass.CHANNEL, } CONTAINER_TYPES_SPECIFIC_MEDIA_CLASS = { - MEDIA_TYPE_APPS: MEDIA_CLASS_DIRECTORY, - MEDIA_TYPE_CHANNELS: MEDIA_CLASS_DIRECTORY, + MediaType.APPS: MediaClass.DIRECTORY, + MediaType.CHANNELS: MediaClass.DIRECTORY, } PLAYABLE_MEDIA_TYPES = [ - MEDIA_TYPE_APP, - MEDIA_TYPE_CHANNEL, + MediaType.APP, + MediaType.CHANNEL, ] EXPANDABLE_MEDIA_TYPES = [ - MEDIA_TYPE_APPS, - MEDIA_TYPE_CHANNELS, + MediaType.APPS, + MediaType.CHANNELS, ] GetBrowseImageUrlType = Callable[[str, str, "str | None"], str] @@ -58,13 +52,13 @@ def get_thumbnail_url_full( ) -> str | None: """Get thumbnail URL.""" if is_internal: - if media_content_type == MEDIA_TYPE_APP and media_content_id: + if media_content_type == MediaType.APP and media_content_id: return coordinator.roku.app_icon_url(media_content_id) return None return get_browse_image_url( media_content_type, - quote_plus(media_content_id), + media_content_id, media_image_id, ) @@ -120,7 +114,7 @@ async def root_payload( children = [ item_payload( - {"title": "Apps", "type": MEDIA_TYPE_APPS}, + {"title": "Apps", "type": MediaType.APPS}, coordinator, get_browse_image_url, ) @@ -129,7 +123,7 @@ async def root_payload( if device.info.device_type == "tv" and len(device.channels) > 0: children.append( item_payload( - {"title": "TV Channels", "type": MEDIA_TYPE_CHANNELS}, + {"title": "TV Channels", "type": MediaType.CHANNELS}, coordinator, get_browse_image_url, ) @@ -161,7 +155,7 @@ async def root_payload( return BrowseMedia( title="Roku", - media_class=MEDIA_CLASS_DIRECTORY, + media_class=MediaClass.DIRECTORY, media_content_id="", media_content_type="root", can_play=False, @@ -184,31 +178,31 @@ def build_item_response( media = None children_media_class = None - if search_type == MEDIA_TYPE_APPS: + if search_type == MediaType.APPS: title = "Apps" media = [ - {"app_id": item.app_id, "title": item.name, "type": MEDIA_TYPE_APP} + {"app_id": item.app_id, "title": item.name, "type": MediaType.APP} for item in coordinator.data.apps ] - children_media_class = MEDIA_CLASS_APP - elif search_type == MEDIA_TYPE_CHANNELS: + children_media_class = MediaClass.APP + elif search_type == MediaType.CHANNELS: title = "TV Channels" media = [ { "channel_number": channel.number, "title": format_channel_name(channel.number, channel.name), - "type": MEDIA_TYPE_CHANNEL, + "type": MediaType.CHANNEL, } for channel in coordinator.data.channels ] - children_media_class = MEDIA_CLASS_CHANNEL + children_media_class = MediaClass.CHANNEL if title is None or media is None: return None return BrowseMedia( media_class=CONTAINER_TYPES_SPECIFIC_MEDIA_CLASS.get( - search_type, MEDIA_CLASS_DIRECTORY + search_type, MediaClass.DIRECTORY ), media_content_id=search_id, media_content_type=search_type, @@ -236,11 +230,11 @@ def item_payload( thumbnail = None if "app_id" in item: - media_content_type = MEDIA_TYPE_APP + media_content_type = MediaType.APP media_content_id = item["app_id"] thumbnail = get_browse_image_url(media_content_type, media_content_id, None) elif "channel_number" in item: - media_content_type = MEDIA_TYPE_CHANNEL + media_content_type = MediaType.CHANNEL media_content_id = item["channel_number"] else: media_content_type = item["type"] diff --git a/homeassistant/components/roku/media_player.py b/homeassistant/components/roku/media_player.py index d9866a3d77a..7b495247c4d 100644 --- a/homeassistant/components/roku/media_player.py +++ b/homeassistant/components/roku/media_player.py @@ -12,30 +12,18 @@ import yarl from homeassistant.components import media_source from homeassistant.components.media_player import ( + ATTR_MEDIA_EXTRA, BrowseMedia, MediaPlayerDeviceClass, MediaPlayerEntity, MediaPlayerEntityFeature, + MediaPlayerState, + MediaType, async_process_play_media_url, ) -from homeassistant.components.media_player.const import ( - ATTR_MEDIA_EXTRA, - MEDIA_TYPE_APP, - MEDIA_TYPE_CHANNEL, - MEDIA_TYPE_MUSIC, - MEDIA_TYPE_URL, - MEDIA_TYPE_VIDEO, -) -from homeassistant.components.stream.const import FORMAT_CONTENT_TYPE, HLS_PROVIDER +from homeassistant.components.stream import FORMAT_CONTENT_TYPE, HLS_PROVIDER from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - ATTR_NAME, - STATE_IDLE, - STATE_ON, - STATE_PAUSED, - STATE_PLAYING, - STATE_STANDBY, -) +from homeassistant.const import ATTR_NAME from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_platform from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -59,16 +47,16 @@ _LOGGER = logging.getLogger(__name__) STREAM_FORMAT_TO_MEDIA_TYPE = { - "dash": MEDIA_TYPE_VIDEO, - "hls": MEDIA_TYPE_VIDEO, - "ism": MEDIA_TYPE_VIDEO, - "m4a": MEDIA_TYPE_MUSIC, - "m4v": MEDIA_TYPE_VIDEO, - "mka": MEDIA_TYPE_MUSIC, - "mkv": MEDIA_TYPE_VIDEO, - "mks": MEDIA_TYPE_VIDEO, - "mp3": MEDIA_TYPE_MUSIC, - "mp4": MEDIA_TYPE_VIDEO, + "dash": MediaType.VIDEO, + "hls": MediaType.VIDEO, + "ism": MediaType.VIDEO, + "m4a": MediaType.MUSIC, + "m4v": MediaType.VIDEO, + "mka": MediaType.MUSIC, + "mkv": MediaType.VIDEO, + "mks": MediaType.VIDEO, + "mp3": MediaType.MUSIC, + "mp4": MediaType.VIDEO, } ATTRS_TO_LAUNCH_PARAMS = { @@ -150,10 +138,10 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity): return MediaPlayerDeviceClass.RECEIVER @property - def state(self) -> str | None: + def state(self) -> MediaPlayerState | None: """Return the state of the device.""" if self.coordinator.data.state.standby: - return STATE_STANDBY + return MediaPlayerState.STANDBY if self.coordinator.data.app is None: return None @@ -163,28 +151,28 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity): or self.coordinator.data.app.name == "Roku" or self.coordinator.data.app.screensaver ): - return STATE_IDLE + return MediaPlayerState.IDLE if self.coordinator.data.media: if self.coordinator.data.media.paused: - return STATE_PAUSED - return STATE_PLAYING + return MediaPlayerState.PAUSED + return MediaPlayerState.PLAYING if self.coordinator.data.app.name: - return STATE_ON + return MediaPlayerState.ON return None @property - def media_content_type(self) -> str | None: + def media_content_type(self) -> MediaType | None: """Content type of current playing media.""" if self.app_id is None or self.app_name in ("Power Saver", "Roku"): return None if self.app_id == "tvinput.dtv" and self.coordinator.data.channel is not None: - return MEDIA_TYPE_CHANNEL + return MediaType.CHANNEL - return MEDIA_TYPE_APP + return MediaType.APP @property def media_image_url(self) -> str | None: @@ -282,7 +270,7 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity): media_image_id: str | None = None, ) -> tuple[bytes | None, str | None]: """Fetch media browser image to serve via proxy.""" - if media_content_type == MEDIA_TYPE_APP and media_content_id: + if media_content_type == MediaType.APP and media_content_id: image_url = self.coordinator.roku.app_icon_url(media_content_id) return await self._async_fetch_image(image_url) @@ -317,21 +305,21 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity): @roku_exception_handler() async def async_media_pause(self) -> None: """Send pause command.""" - if self.state not in (STATE_STANDBY, STATE_PAUSED): + if self.state not in {MediaPlayerState.STANDBY, MediaPlayerState.PAUSED}: await self.coordinator.roku.remote("play") await self.coordinator.async_request_refresh() @roku_exception_handler() async def async_media_play(self) -> None: """Send play command.""" - if self.state not in (STATE_STANDBY, STATE_PLAYING): + if self.state not in {MediaPlayerState.STANDBY, MediaPlayerState.PLAYING}: await self.coordinator.roku.remote("play") await self.coordinator.async_request_refresh() @roku_exception_handler() async def async_media_play_pause(self) -> None: """Send play/pause command.""" - if self.state != STATE_STANDBY: + if self.state != MediaPlayerState.STANDBY: await self.coordinator.roku.remote("play") await self.coordinator.async_request_refresh() @@ -380,19 +368,19 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity): sourced_media = await media_source.async_resolve_media( self.hass, media_id, self.entity_id ) - media_type = MEDIA_TYPE_URL + media_type = MediaType.URL media_id = sourced_media.url mime_type = sourced_media.mime_type stream_name = original_media_id stream_format = guess_stream_format(media_id, mime_type) if media_type == FORMAT_CONTENT_TYPE[HLS_PROVIDER]: - media_type = MEDIA_TYPE_VIDEO + media_type = MediaType.VIDEO mime_type = FORMAT_CONTENT_TYPE[HLS_PROVIDER] stream_name = "Camera Stream" stream_format = "hls" - if media_type in (MEDIA_TYPE_MUSIC, MEDIA_TYPE_URL, MEDIA_TYPE_VIDEO): + if media_type in {MediaType.MUSIC, MediaType.URL, MediaType.VIDEO}: # If media ID is a relative URL, we serve it from HA. media_id = async_process_play_media_url(self.hass, media_id) @@ -417,12 +405,12 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity): return if ( - media_type == MEDIA_TYPE_URL - and STREAM_FORMAT_TO_MEDIA_TYPE[extra[ATTR_FORMAT]] == MEDIA_TYPE_MUSIC + media_type == MediaType.URL + and STREAM_FORMAT_TO_MEDIA_TYPE[extra[ATTR_FORMAT]] == MediaType.MUSIC ): - media_type = MEDIA_TYPE_MUSIC + media_type = MediaType.MUSIC - if media_type == MEDIA_TYPE_MUSIC and "tts_proxy" in media_id: + if media_type == MediaType.MUSIC and "tts_proxy" in media_id: stream_name = "Text to Speech" elif stream_name is None: if stream_format == "ism": @@ -433,7 +421,7 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity): if extra.get(ATTR_NAME) is None: extra[ATTR_NAME] = stream_name - if media_type == MEDIA_TYPE_APP: + if media_type == MediaType.APP: params = { param: extra[attr] for attr, param in ATTRS_TO_LAUNCH_PARAMS.items() @@ -441,9 +429,9 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity): } await self.coordinator.roku.launch(media_id, params) - elif media_type == MEDIA_TYPE_CHANNEL: + elif media_type == MediaType.CHANNEL: await self.coordinator.roku.tune(media_id) - elif media_type == MEDIA_TYPE_MUSIC: + elif media_type == MediaType.MUSIC: if extra.get(ATTR_ARTIST_NAME) is None: extra[ATTR_ARTIST_NAME] = "Home Assistant" @@ -456,7 +444,7 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity): params = {"t": "a", **params} await self.coordinator.roku.play_on_roku(media_id, params) - elif media_type in (MEDIA_TYPE_URL, MEDIA_TYPE_VIDEO): + elif media_type in {MediaType.URL, MediaType.VIDEO}: params = { param: extra[attr] for (attr, param) in ATTRS_TO_PLAY_ON_ROKU_PARAMS.items() diff --git a/homeassistant/components/roomba/strings.json b/homeassistant/components/roomba/strings.json index b52d6443213..a644797c1be 100644 --- a/homeassistant/components/roomba/strings.json +++ b/homeassistant/components/roomba/strings.json @@ -18,11 +18,11 @@ }, "link": { "title": "Retrieve Password", - "description": "Press and hold the Home button on {name} until the device generates a sound (about two seconds), then submit within 30 seconds." + "description": "Make sure that the iRobot app is not running on any device. Press and hold the Home button on {name} until the device generates a sound (about two seconds), then submit within 30 seconds." }, "link_manual": { "title": "Enter Password", - "description": "The password could not be retrieved from the device automatically. Please follow the steps outlined in the documentation at: {auth_help_url}", + "description": "The password could not be retrieved from the device automatically. Please make sure that the iRobot app is not open on any device while trying to retrieve the password. Please follow the steps outlined in the documentation at: {auth_help_url}", "data": { "password": "[%key:common::config_flow::data::password%]" } diff --git a/homeassistant/components/roon/media_browser.py b/homeassistant/components/roon/media_browser.py index 2f132ee9d23..dd7a2a1faa3 100644 --- a/homeassistant/components/roon/media_browser.py +++ b/homeassistant/components/roon/media_browser.py @@ -1,12 +1,7 @@ """Support to interface with the Roon API.""" import logging -from homeassistant.components.media_player import BrowseMedia -from homeassistant.components.media_player.const import ( - MEDIA_CLASS_DIRECTORY, - MEDIA_CLASS_PLAYLIST, - MEDIA_CLASS_TRACK, -) +from homeassistant.components.media_player import BrowseMedia, MediaClass from homeassistant.components.media_player.errors import BrowseError @@ -71,18 +66,18 @@ def item_payload(roon_server, item, list_image_id): hint = item.get("hint") if hint == "list": - media_class = MEDIA_CLASS_DIRECTORY + media_class = MediaClass.DIRECTORY can_expand = True elif hint == "action_list": - media_class = MEDIA_CLASS_PLAYLIST + media_class = MediaClass.PLAYLIST can_expand = False elif hint == "action": media_content_type = "track" - media_class = MEDIA_CLASS_TRACK + media_class = MediaClass.TRACK can_expand = False else: # Roon API says to treat unknown as a list - media_class = MEDIA_CLASS_DIRECTORY + media_class = MediaClass.DIRECTORY can_expand = True _LOGGER.warning("Unknown hint %s - %s", title, hint) @@ -135,7 +130,7 @@ def library_payload(roon_server, zone_id, media_content_id): title=list_title, media_content_id=content_id, media_content_type="library", - media_class=MEDIA_CLASS_DIRECTORY, + media_class=MediaClass.DIRECTORY, can_play=False, can_expand=True, children=[], diff --git a/homeassistant/components/roon/media_player.py b/homeassistant/components/roon/media_player.py index 673316f64a3..c80f92834bb 100644 --- a/homeassistant/components/roon/media_player.py +++ b/homeassistant/components/roon/media_player.py @@ -1,21 +1,20 @@ """MediaPlayer platform for Roon integration.""" +from __future__ import annotations + import logging +from typing import Any from roonapi import split_media_path import voluptuous as vol from homeassistant.components.media_player import ( + BrowseMedia, MediaPlayerEntity, MediaPlayerEntityFeature, + MediaPlayerState, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - DEVICE_DEFAULT_NAME, - STATE_IDLE, - STATE_OFF, - STATE_PAUSED, - STATE_PLAYING, -) +from homeassistant.const import DEVICE_DEFAULT_NAME from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.dispatcher import ( @@ -102,7 +101,7 @@ class RoonDevice(MediaPlayerEntity): self._available = True self._last_position_update = None self._supports_standby = False - self._state = STATE_IDLE + self._state = MediaPlayerState.IDLE self._unique_id = None self._zone_id = None self._output_id = None @@ -119,7 +118,7 @@ class RoonDevice(MediaPlayerEntity): self._volume_level = 0 self.update_data(player_data) - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register callback.""" self.async_on_remove( async_dispatcher_connect( @@ -168,12 +167,12 @@ class RoonDevice(MediaPlayerEntity): if not self.player_data["is_available"]: # this player was removed self._available = False - self._state = STATE_OFF + self._state = MediaPlayerState.OFF else: self._available = True # determine player state self.update_state() - if self.state == STATE_PLAYING: + if self.state == MediaPlayerState.PLAYING: self._last_position_update = utcnow() @classmethod @@ -250,20 +249,20 @@ class RoonDevice(MediaPlayerEntity): if source["supports_standby"] and source["status"] != "indeterminate": self._supports_standby = True if source["status"] in ["standby", "deselected"]: - new_state = STATE_OFF + new_state = MediaPlayerState.OFF break # determine player state if not new_state: if self.player_data["state"] == "playing": - new_state = STATE_PLAYING + new_state = MediaPlayerState.PLAYING elif self.player_data["state"] == "loading": - new_state = STATE_PLAYING + new_state = MediaPlayerState.PLAYING elif self.player_data["state"] == "stopped": - new_state = STATE_IDLE + new_state = MediaPlayerState.IDLE elif self.player_data["state"] == "paused": - new_state = STATE_PAUSED + new_state = MediaPlayerState.PAUSED else: - new_state = STATE_IDLE + new_state = MediaPlayerState.IDLE self._state = new_state self._unique_id = self.player_data["dev_id"] self._zone_id = self.player_data["zone_id"] @@ -376,38 +375,38 @@ class RoonDevice(MediaPlayerEntity): """Boolean if shuffle is enabled.""" return self._shuffle - def media_play(self): + def media_play(self) -> None: """Send play command to device.""" self._server.roonapi.playback_control(self.output_id, "play") - def media_pause(self): + def media_pause(self) -> None: """Send pause command to device.""" self._server.roonapi.playback_control(self.output_id, "pause") - def media_play_pause(self): + def media_play_pause(self) -> None: """Toggle play command to device.""" self._server.roonapi.playback_control(self.output_id, "playpause") - def media_stop(self): + def media_stop(self) -> None: """Send stop command to device.""" self._server.roonapi.playback_control(self.output_id, "stop") - def media_next_track(self): + def media_next_track(self) -> None: """Send next track command to device.""" self._server.roonapi.playback_control(self.output_id, "next") - def media_previous_track(self): + def media_previous_track(self) -> None: """Send previous track command to device.""" self._server.roonapi.playback_control(self.output_id, "previous") - def media_seek(self, position): + def media_seek(self, position: float) -> None: """Send seek command to device.""" self._server.roonapi.seek(self.output_id, position) # Seek doesn't cause an async update - so force one self._media_position = position self.schedule_update_ha_state() - def set_volume_level(self, volume): + 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) @@ -416,15 +415,15 @@ class RoonDevice(MediaPlayerEntity): """Send mute/unmute to device.""" self._server.roonapi.mute(self.output_id, mute) - def volume_up(self): + def volume_up(self) -> None: """Send new volume_level to device.""" self._server.roonapi.change_volume(self.output_id, 3, "relative") - def volume_down(self): + def volume_down(self) -> None: """Send new volume_level to device.""" self._server.roonapi.change_volume(self.output_id, -3, "relative") - def turn_on(self): + def turn_on(self) -> None: """Turn on device (if supported).""" if not (self.supports_standby and "source_controls" in self.player_data): self.media_play() @@ -436,7 +435,7 @@ class RoonDevice(MediaPlayerEntity): ) return - def turn_off(self): + def turn_off(self) -> None: """Turn off device (if supported).""" if not (self.supports_standby and "source_controls" in self.player_data): self.media_stop() @@ -447,11 +446,11 @@ class RoonDevice(MediaPlayerEntity): self._server.roonapi.standby(self.output_id, source["control_key"]) return - def set_shuffle(self, shuffle): + def set_shuffle(self, shuffle: bool) -> None: """Set shuffle state.""" self._server.roonapi.shuffle(self.output_id, shuffle) - def play_media(self, media_type, media_id, **kwargs): + def play_media(self, media_type: str, media_id: str, **kwargs: Any) -> None: """Send the play_media command to the media player.""" _LOGGER.debug("Playback request for %s / %s", media_type, media_id) @@ -469,7 +468,7 @@ class RoonDevice(MediaPlayerEntity): path_list, ) - def join_players(self, group_members): + def join_players(self, group_members: list[str]) -> None: """Join `group_members` as a player group with the current player.""" zone_data = self._server.roonapi.zone_by_output_id(self._output_id) @@ -509,7 +508,7 @@ class RoonDevice(MediaPlayerEntity): [self._output_id] + [sync_available[name] for name in names] ) - def unjoin_player(self): + def unjoin_player(self) -> None: """Remove this player from any group.""" if not self._server.roonapi.is_grouped(self._output_id): @@ -548,7 +547,9 @@ class RoonDevice(MediaPlayerEntity): self._server.roonapi.transfer_zone, self._zone_id, transfer_id ) - 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.""" return await self.hass.async_add_executor_job( browse_media, diff --git a/homeassistant/components/roon/translations/cs.json b/homeassistant/components/roon/translations/cs.json index 150b2f5f74a..d0e8755aa8d 100644 --- a/homeassistant/components/roon/translations/cs.json +++ b/homeassistant/components/roon/translations/cs.json @@ -8,6 +8,11 @@ "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, "step": { + "fallback": { + "data": { + "host": "Hostitel" + } + }, "link": { "description": "Mus\u00edte povolit Home Assistant v Roon. Po kliknut\u00ed na Odeslat p\u0159ejd\u011bte do aplikace Roon Core, otev\u0159ete Nastaven\u00ed a na z\u00e1lo\u017ece Roz\u0161\u00ed\u0159en\u00ed povolte Home Assistant.", "title": "Autorizujte HomeAssistant v Roon" diff --git a/homeassistant/components/roon/translations/el.json b/homeassistant/components/roon/translations/el.json index 1216ff032a0..058bd0369a6 100644 --- a/homeassistant/components/roon/translations/el.json +++ b/homeassistant/components/roon/translations/el.json @@ -18,6 +18,10 @@ "link": { "description": "\u03a0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03bf\u03c4\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf Home Assistant \u03c3\u03c4\u03bf Roon. \u0391\u03c6\u03bf\u03cd \u03ba\u03ac\u03bd\u03b5\u03c4\u03b5 \u03ba\u03bb\u03b9\u03ba \u03c3\u03c4\u03b7\u03bd \u03c5\u03c0\u03bf\u03b2\u03bf\u03bb\u03ae, \u03bc\u03b5\u03c4\u03b1\u03b2\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03b7\u03bd \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae Roon Core, \u03b1\u03bd\u03bf\u03af\u03be\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 \u03ba\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf HomeAssistant \u03c3\u03c4\u03b7\u03bd \u03ba\u03b1\u03c1\u03c4\u03ad\u03bb\u03b1 \u0395\u03c0\u03b5\u03ba\u03c4\u03ac\u03c3\u03b5\u03b9\u03c2.", "title": "\u0395\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03bf\u03c4\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf HomeAssistant \u03c3\u03c4\u03bf Roon" + }, + "user": { + "one": "\u03ba\u03b5\u03bd\u03cc", + "other": "\u03ba\u03b5\u03bd\u03cc" } } } diff --git a/homeassistant/components/roon/translations/tr.json b/homeassistant/components/roon/translations/tr.json index a05dfed70f1..c205364c22b 100644 --- a/homeassistant/components/roon/translations/tr.json +++ b/homeassistant/components/roon/translations/tr.json @@ -18,6 +18,10 @@ "link": { "description": "Roon'da HomeAssistant\u0131 yetkilendirmelisiniz. G\u00f6nder'e t\u0131klad\u0131ktan sonra, Roon Core uygulamas\u0131na gidin, Ayarlar'\u0131 a\u00e7\u0131n ve Uzant\u0131lar sekmesinde HomeAssistant'\u0131 etkinle\u015ftirin.", "title": "Roon'da HomeAssistant'\u0131 Yetkilendirme" + }, + "user": { + "one": "Bo\u015f", + "other": "Bo\u015f" } } } diff --git a/homeassistant/components/rova/sensor.py b/homeassistant/components/rova/sensor.py index 1057d9b5ea8..26f6d67e697 100644 --- a/homeassistant/components/rova/sensor.py +++ b/homeassistant/components/rova/sensor.py @@ -117,7 +117,7 @@ class RovaSensor(SensorEntity): self._attr_name = f"{platform_name}_{description.name}" self._attr_device_class = SensorDeviceClass.TIMESTAMP - def update(self): + def update(self) -> None: """Get the latest data from the sensor and update the state.""" self.data_service.update() pickup_date = self.data_service.data.get(self.entity_description.key) diff --git a/homeassistant/components/rtsp_to_webrtc/__init__.py b/homeassistant/components/rtsp_to_webrtc/__init__.py index 185cfcb0240..f0e013fc02f 100644 --- a/homeassistant/components/rtsp_to_webrtc/__init__.py +++ b/homeassistant/components/rtsp_to_webrtc/__init__.py @@ -24,10 +24,11 @@ import async_timeout from rtsp_to_webrtc.client import get_adaptive_client from rtsp_to_webrtc.exceptions import ClientError, ResponseError from rtsp_to_webrtc.interface import WebRTCClientInterface +import voluptuous as vol -from homeassistant.components import camera +from homeassistant.components import camera, websocket_api from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -37,6 +38,7 @@ DOMAIN = "rtsp_to_webrtc" DATA_SERVER_URL = "server_url" DATA_UNSUB = "unsub" TIMEOUT = 10 +CONF_STUN_SERVER = "stun_server" async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -54,6 +56,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except (TimeoutError, ClientError) as err: raise ConfigEntryNotReady from err + hass.data[DOMAIN][CONF_STUN_SERVER] = entry.options.get(CONF_STUN_SERVER, "") + async def async_offer_for_stream_source( stream_source: str, offer_sdp: str, @@ -78,10 +82,37 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass, DOMAIN, async_offer_for_stream_source ) ) + entry.async_on_unload(entry.add_update_listener(async_reload_entry)) + + websocket_api.async_register_command(hass, ws_get_settings) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" + if DOMAIN in hass.data: + del hass.data[DOMAIN] return True + + +async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Reload config entry when options change.""" + if hass.data[DOMAIN][CONF_STUN_SERVER] != entry.options.get(CONF_STUN_SERVER, ""): + await hass.config_entries.async_reload(entry.entry_id) + + +@websocket_api.websocket_command( + { + vol.Required("type"): "rtsp_to_webrtc/get_settings", + } +) +@callback +def ws_get_settings( + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict +) -> None: + """Handle the websocket command.""" + connection.send_result( + msg["id"], + {CONF_STUN_SERVER: hass.data.get(DOMAIN, {}).get(CONF_STUN_SERVER, "")}, + ) diff --git a/homeassistant/components/rtsp_to_webrtc/config_flow.py b/homeassistant/components/rtsp_to_webrtc/config_flow.py index 815c5e5db7b..865a6bafcb6 100644 --- a/homeassistant/components/rtsp_to_webrtc/config_flow.py +++ b/homeassistant/components/rtsp_to_webrtc/config_flow.py @@ -11,10 +11,11 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.components.hassio import HassioServiceInfo from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession -from . import DATA_SERVER_URL, DOMAIN +from . import CONF_STUN_SERVER, DATA_SERVER_URL, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -104,3 +105,42 @@ class RTSPToWebRTCConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): title=self._hassio_discovery["addon"], data={DATA_SERVER_URL: url}, ) + + @staticmethod + @callback + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> config_entries.OptionsFlow: + """Create an options flow.""" + return OptionsFlowHandler(config_entry) + + +class OptionsFlowHandler(config_entries.OptionsFlow): + """RTSPtoWeb Options flow.""" + + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + """Initialize options flow.""" + self.config_entry = config_entry + + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Manage the options.""" + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + return self.async_show_form( + step_id="init", + data_schema=vol.Schema( + { + vol.Optional( + CONF_STUN_SERVER, + description={ + "suggested_value": self.config_entry.options.get( + CONF_STUN_SERVER + ), + }, + ): str, + } + ), + ) diff --git a/homeassistant/components/rtsp_to_webrtc/strings.json b/homeassistant/components/rtsp_to_webrtc/strings.json index 5ef91eaf206..939c30766e2 100644 --- a/homeassistant/components/rtsp_to_webrtc/strings.json +++ b/homeassistant/components/rtsp_to_webrtc/strings.json @@ -23,5 +23,14 @@ "server_failure": "RTSPtoWebRTC server returned an error. Check logs for more information.", "server_unreachable": "Unable to communicate with RTSPtoWebRTC server. Check logs for more information." } + }, + "options": { + "step": { + "init": { + "data": { + "stun_server": "Stun server address (host:port)" + } + } + } } } diff --git a/homeassistant/components/rtsp_to_webrtc/translations/en.json b/homeassistant/components/rtsp_to_webrtc/translations/en.json index c54983d63d3..a519883b764 100644 --- a/homeassistant/components/rtsp_to_webrtc/translations/en.json +++ b/homeassistant/components/rtsp_to_webrtc/translations/en.json @@ -23,5 +23,14 @@ "title": "Configure RTSPtoWebRTC" } } + }, + "options": { + "step": { + "init": { + "data": { + "stun_server": "Stun server address (host:port)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/russound_rio/media_player.py b/homeassistant/components/russound_rio/media_player.py index e905ab0c726..c639e5ddc90 100644 --- a/homeassistant/components/russound_rio/media_player.py +++ b/homeassistant/components/russound_rio/media_player.py @@ -8,15 +8,14 @@ from homeassistant.components.media_player import ( PLATFORM_SCHEMA, MediaPlayerEntity, MediaPlayerEntityFeature, + MediaPlayerState, + MediaType, ) -from homeassistant.components.media_player.const import MEDIA_TYPE_MUSIC from homeassistant.const import ( CONF_HOST, CONF_NAME, CONF_PORT, EVENT_HOMEASSISTANT_STOP, - STATE_OFF, - STATE_ON, ) from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv @@ -70,6 +69,7 @@ async def async_setup_platform( class RussoundZoneDevice(MediaPlayerEntity): """Representation of a Russound Zone.""" + _attr_media_content_type = MediaType.MUSIC _attr_should_poll = False _attr_supported_features = ( MediaPlayerEntityFeature.VOLUME_MUTE @@ -115,7 +115,7 @@ class RussoundZoneDevice(MediaPlayerEntity): if source_id == current: self.schedule_update_ha_state() - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register callback handlers.""" self._russ.add_zone_callback(self._zone_callback_handler) self._russ.add_source_callback(self._source_callback_handler) @@ -130,9 +130,9 @@ class RussoundZoneDevice(MediaPlayerEntity): """Return the state of the device.""" status = self._zone_var("status", "OFF") if status == "ON": - return STATE_ON + return MediaPlayerState.ON if status == "OFF": - return STATE_OFF + return MediaPlayerState.OFF @property def source(self): @@ -144,11 +144,6 @@ class RussoundZoneDevice(MediaPlayerEntity): """Return a list of available input sources.""" return [x[1] for x in self._sources] - @property - def media_content_type(self): - """Content type of current playing media.""" - return MEDIA_TYPE_MUSIC - @property def media_title(self): """Title of current playing media.""" @@ -178,20 +173,20 @@ class RussoundZoneDevice(MediaPlayerEntity): """ return float(self._zone_var("volume", 0)) / 50.0 - async def async_turn_off(self): + async def async_turn_off(self) -> None: """Turn off the zone.""" await self._russ.send_zone_event(self._zone_id, "ZoneOff") - async def async_turn_on(self): + async def async_turn_on(self) -> None: """Turn on the zone.""" await self._russ.send_zone_event(self._zone_id, "ZoneOn") - async def async_set_volume_level(self, volume): + async def async_set_volume_level(self, volume: float) -> None: """Set the volume level.""" rvol = int(volume * 50.0) await self._russ.send_zone_event(self._zone_id, "KeyPress", "Volume", rvol) - async def async_select_source(self, source): + async def async_select_source(self, source: str) -> None: """Select the source input for this zone.""" for source_id, name in self._sources: if name.lower() != source.lower(): diff --git a/homeassistant/components/russound_rnet/media_player.py b/homeassistant/components/russound_rnet/media_player.py index b97b431333f..6782d783a83 100644 --- a/homeassistant/components/russound_rnet/media_player.py +++ b/homeassistant/components/russound_rnet/media_player.py @@ -10,8 +10,9 @@ from homeassistant.components.media_player import ( PLATFORM_SCHEMA, MediaPlayerEntity, MediaPlayerEntityFeature, + MediaPlayerState, ) -from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, STATE_OFF, STATE_ON +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -90,7 +91,7 @@ class RussoundRNETDevice(MediaPlayerEntity): self._volume = None self._source = None - def update(self): + def update(self) -> None: """Retrieve latest state.""" # Updated this function to make a single call to get_zone_info, so that # with a single call we can get On/Off, Volume and Source, reducing the @@ -100,9 +101,9 @@ class RussoundRNETDevice(MediaPlayerEntity): if ret is not None: _LOGGER.debug("Updating status for zone %s", self._zone_id) if ret[0] == 0: - self._state = STATE_OFF + self._state = MediaPlayerState.OFF else: - self._state = STATE_ON + self._state = MediaPlayerState.ON self._volume = ret[2] * 2 / 100.0 # Returns 0 based index for source. index = ret[1] @@ -141,7 +142,7 @@ class RussoundRNETDevice(MediaPlayerEntity): """ return self._volume - def set_volume_level(self, volume): + def set_volume_level(self, volume: float) -> None: """Set volume level. Volume has a range (0..1). Translate this to a range of (0..100) as expected @@ -149,19 +150,19 @@ class RussoundRNETDevice(MediaPlayerEntity): """ self._russ.set_volume("1", self._zone_id, volume * 100) - def turn_on(self): + def turn_on(self) -> None: """Turn the media player on.""" self._russ.set_power("1", self._zone_id, "1") - def turn_off(self): + def turn_off(self) -> None: """Turn off media player.""" self._russ.set_power("1", self._zone_id, "0") - def mute_volume(self, mute): + def mute_volume(self, mute: bool) -> None: """Send mute command.""" self._russ.toggle_mute("1", self._zone_id) - def select_source(self, source): + def select_source(self, source: str) -> None: """Set the input source.""" if source in self._sources: index = self._sources.index(source) diff --git a/homeassistant/components/sabnzbd/sensor.py b/homeassistant/components/sabnzbd/sensor.py index b2828a30969..b4231a469ec 100644 --- a/homeassistant/components/sabnzbd/sensor.py +++ b/homeassistant/components/sabnzbd/sensor.py @@ -167,7 +167,7 @@ class SabnzbdSensor(SensorEntity): name=DEFAULT_NAME, ) - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Call when entity about to be added to hass.""" self.async_on_remove( async_dispatcher_connect( diff --git a/homeassistant/components/sabnzbd/translations/cs.json b/homeassistant/components/sabnzbd/translations/cs.json new file mode 100644 index 00000000000..764f65efebe --- /dev/null +++ b/homeassistant/components/sabnzbd/translations/cs.json @@ -0,0 +1,8 @@ +{ + "config": { + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_api_key": "Neplatn\u00fd kl\u00ed\u010d API" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/safe_mode/manifest.json b/homeassistant/components/safe_mode/manifest.json index 5ce7c3abf7b..f2627693f33 100644 --- a/homeassistant/components/safe_mode/manifest.json +++ b/homeassistant/components/safe_mode/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/safe_mode", "dependencies": ["frontend", "persistent_notification", "cloud"], "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "system" } diff --git a/homeassistant/components/samsungtv/media_player.py b/homeassistant/components/samsungtv/media_player.py index cf3bfcd64a1..3e544b181f1 100644 --- a/homeassistant/components/samsungtv/media_player.py +++ b/homeassistant/components/samsungtv/media_player.py @@ -26,20 +26,11 @@ from homeassistant.components.media_player import ( MediaPlayerDeviceClass, MediaPlayerEntity, MediaPlayerEntityFeature, -) -from homeassistant.components.media_player.const import ( - MEDIA_TYPE_APP, - MEDIA_TYPE_CHANNEL, + MediaPlayerState, + MediaType, ) from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry -from homeassistant.const import ( - CONF_HOST, - CONF_MAC, - CONF_MODEL, - CONF_NAME, - STATE_OFF, - STATE_ON, -) +from homeassistant.const import CONF_HOST, CONF_MAC, CONF_MODEL, CONF_NAME from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_component from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -125,7 +116,6 @@ class SamsungTVDevice(MediaPlayerEntity): self._playing: bool = True self._attr_name: str | None = config_entry.data.get(CONF_NAME) - self._attr_state: str | None = None self._attr_unique_id = config_entry.unique_id self._attr_is_volume_muted: bool = False self._attr_device_class = MediaPlayerDeviceClass.TV @@ -199,15 +189,17 @@ class SamsungTVDevice(MediaPlayerEntity): return old_state = self._attr_state if self._power_off_in_progress(): - self._attr_state = STATE_OFF + self._attr_state = MediaPlayerState.OFF else: self._attr_state = ( - STATE_ON if await self._bridge.async_is_on() else STATE_OFF + MediaPlayerState.ON + if await self._bridge.async_is_on() + else MediaPlayerState.OFF ) if self._attr_state != old_state: - LOGGER.debug("TV %s state updated to %s", self._host, self._attr_state) + LOGGER.debug("TV %s state updated to %s", self._host, self.state) - if self._attr_state != STATE_ON: + if self._attr_state != MediaPlayerState.ON: if self._dmr_device and self._dmr_device.is_subscribed: await self._dmr_device.async_unsubscribe_services() return @@ -364,7 +356,7 @@ class SamsungTVDevice(MediaPlayerEntity): if self._auth_failed: return False return ( - self._attr_state == STATE_ON + self.state == MediaPlayerState.ON or self._on_script is not None or self._mac is not None or self._power_off_in_progress() @@ -426,11 +418,11 @@ class SamsungTVDevice(MediaPlayerEntity): self, media_type: str, media_id: str, **kwargs: Any ) -> None: """Support changing a channel.""" - if media_type == MEDIA_TYPE_APP: + if media_type == MediaType.APP: await self._async_launch_app(media_id) return - if media_type != MEDIA_TYPE_CHANNEL: + if media_type != MediaType.CHANNEL: LOGGER.error("Unsupported media type") return diff --git a/homeassistant/components/samsungtv/translations/es.json b/homeassistant/components/samsungtv/translations/es.json index b102c85b2f3..e6ff4576e54 100644 --- a/homeassistant/components/samsungtv/translations/es.json +++ b/homeassistant/components/samsungtv/translations/es.json @@ -7,7 +7,7 @@ "cannot_connect": "No se pudo conectar", "id_missing": "Este dispositivo Samsung no tiene un n\u00famero de serie.", "not_supported": "Este dispositivo Samsung no es compatible por el momento.", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente", "unknown": "Error inesperado" }, "error": { diff --git a/homeassistant/components/samsungtv/translations/ja.json b/homeassistant/components/samsungtv/translations/ja.json index 6b0cae1ba4d..547bad1c594 100644 --- a/homeassistant/components/samsungtv/translations/ja.json +++ b/homeassistant/components/samsungtv/translations/ja.json @@ -26,7 +26,7 @@ "description": "{device} \u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f\u3053\u308c\u307e\u3067\u306bHome Assistant\u3092\u4e00\u5ea6\u3082\u63a5\u7d9a\u3057\u305f\u3053\u3068\u304c\u306a\u3044\u5834\u5408\u306f\u3001\u30c6\u30ec\u30d3\u306b\u8a8d\u8a3c\u3092\u6c42\u3081\u308b\u30dd\u30c3\u30d7\u30a2\u30c3\u30d7\u304c\u8868\u793a\u3055\u308c\u307e\u3059\u3002" }, "reauth_confirm": { - "description": "\u9001\u4fe1(submit)\u3001\u8a8d\u8a3c\u3092\u8981\u6c42\u3059\u308b {device} \u306e\u30dd\u30c3\u30d7\u30a2\u30c3\u30d7\u3092\u300130\u79d2\u4ee5\u5185\u306b\u53d7\u3051\u5165\u308c\u307e\u3059\u3002" + "description": "\u9001\u4fe1\u5f8c\u300130 \u79d2\u4ee5\u5185\u306b\u627f\u8a8d\u3092\u8981\u6c42\u3059\u308b{device}\u306e\u30dd\u30c3\u30d7\u30a2\u30c3\u30d7\u3092\u53d7\u3051\u5165\u308c\u308b\u304b\u3001PIN \u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, "reauth_confirm_encrypted": { "description": "{device} \u306b\u8868\u793a\u3055\u308c\u3066\u3044\u308bPIN\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002" diff --git a/homeassistant/components/samsungtv/translations/pt.json b/homeassistant/components/samsungtv/translations/pt.json index c0c3fb735c1..3bfc36fd84d 100644 --- a/homeassistant/components/samsungtv/translations/pt.json +++ b/homeassistant/components/samsungtv/translations/pt.json @@ -5,7 +5,7 @@ "already_in_progress": "O processo de configura\u00e7\u00e3o j\u00e1 est\u00e1 a decorrer", "cannot_connect": "Falha na liga\u00e7\u00e3o" }, - "flow_title": "", + "flow_title": "{device}", "step": { "user": { "data": { diff --git a/homeassistant/components/satel_integra/binary_sensor.py b/homeassistant/components/satel_integra/binary_sensor.py index f55545239fb..389bde884ef 100644 --- a/homeassistant/components/satel_integra/binary_sensor.py +++ b/homeassistant/components/satel_integra/binary_sensor.py @@ -73,7 +73,7 @@ class SatelIntegraBinarySensor(BinarySensorEntity): self._react_to_signal = react_to_signal self._satel = controller - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register callbacks.""" if self._react_to_signal == SIGNAL_OUTPUTS_UPDATED: if self._device_number in self._satel.violated_outputs: diff --git a/homeassistant/components/satel_integra/switch.py b/homeassistant/components/satel_integra/switch.py index 7c7b91c4ac6..469b2280290 100644 --- a/homeassistant/components/satel_integra/switch.py +++ b/homeassistant/components/satel_integra/switch.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any from homeassistant.components.switch import SwitchEntity from homeassistant.core import HomeAssistant, callback @@ -61,7 +62,7 @@ class SatelIntegraSwitch(SwitchEntity): self._code = code self._satel = controller - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register callbacks.""" async_dispatcher_connect( self.hass, SIGNAL_OUTPUTS_UPDATED, self._devices_updated @@ -78,13 +79,13 @@ class SatelIntegraSwitch(SwitchEntity): self._state = new_state self.async_write_ha_state() - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" _LOGGER.debug("Switch: %s status: %s, turning on", self._name, self._state) await self._satel.set_output(self._code, self._device_number, True) self.async_write_ha_state() - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" _LOGGER.debug( "Switch name: %s status: %s, turning off", self._name, self._state diff --git a/homeassistant/components/scene/__init__.py b/homeassistant/components/scene/__init__.py index 5dea5965d43..3c8adbd0502 100644 --- a/homeassistant/components/scene/__init__.py +++ b/homeassistant/components/scene/__init__.py @@ -4,7 +4,7 @@ from __future__ import annotations import functools as ft import importlib import logging -from typing import Any, final +from typing import Any, Final, final import voluptuous as vol @@ -17,13 +17,11 @@ from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType from homeassistant.util import dt as dt_util -# mypy: allow-untyped-defs, no-check-untyped-defs - -DOMAIN = "scene" -STATES = "states" +DOMAIN: Final = "scene" +STATES: Final = "states" -def _hass_domain_validator(config): +def _hass_domain_validator(config: dict[str, Any]) -> dict[str, Any]: """Validate platform in config for homeassistant domain.""" if CONF_PLATFORM not in config: config = {CONF_PLATFORM: HA_DOMAIN, STATES: config} @@ -31,7 +29,7 @@ def _hass_domain_validator(config): return config -def _platform_validator(config): +def _platform_validator(config: dict[str, Any]) -> dict[str, Any]: """Validate it is a valid platform.""" try: platform = importlib.import_module(f".{config[CONF_PLATFORM]}", __name__) @@ -46,7 +44,7 @@ def _platform_validator(config): if not hasattr(platform, "PLATFORM_SCHEMA"): return config - return platform.PLATFORM_SCHEMA(config) + return platform.PLATFORM_SCHEMA(config) # type: ignore[no-any-return] PLATFORM_SCHEMA = vol.Schema( @@ -58,10 +56,12 @@ PLATFORM_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) +# mypy: disallow-any-generics + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the scenes.""" - component = hass.data[DOMAIN] = EntityComponent( + component = hass.data[DOMAIN] = EntityComponent[Scene]( logging.getLogger(__name__), DOMAIN, hass ) @@ -79,13 +79,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry.""" - component: EntityComponent = hass.data[DOMAIN] + component: EntityComponent[Scene] = hass.data[DOMAIN] return await component.async_setup_entry(entry) async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - component: EntityComponent = hass.data[DOMAIN] + component: EntityComponent[Scene] = hass.data[DOMAIN] return await component.async_unload_entry(entry) diff --git a/homeassistant/components/scene/manifest.json b/homeassistant/components/scene/manifest.json index 3134a310042..d653c8076e6 100644 --- a/homeassistant/components/scene/manifest.json +++ b/homeassistant/components/scene/manifest.json @@ -3,5 +3,6 @@ "name": "Scenes", "documentation": "https://www.home-assistant.io/integrations/scene", "codeowners": ["@home-assistant/core"], - "quality_scale": "internal" + "quality_scale": "internal", + "integration_type": "entity" } diff --git a/homeassistant/components/schedule/__init__.py b/homeassistant/components/schedule/__init__.py index 6ad3bcff58e..fefb5189e3c 100644 --- a/homeassistant/components/schedule/__init__.py +++ b/homeassistant/components/schedule/__init__.py @@ -20,6 +20,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.helpers.collection import ( + CollectionEntity, IDManager, StorageCollection, StorageCollectionWebsocket, @@ -27,7 +28,6 @@ from homeassistant.helpers.collection import ( sync_entity_lifecycle, ) import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.helpers.integration_platform import ( @@ -154,7 +154,7 @@ ENTITY_SCHEMA = vol.Schema( async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up an input select.""" - component = EntityComponent(LOGGER, DOMAIN, hass) + component = EntityComponent[Schedule](LOGGER, DOMAIN, hass) # Process integration platforms right away since # we will create entities before firing EVENT_COMPONENT_LOADED @@ -163,9 +163,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: id_manager = IDManager() yaml_collection = YamlCollection(LOGGER, id_manager) - sync_entity_lifecycle( - hass, DOMAIN, DOMAIN, component, yaml_collection, Schedule.from_yaml - ) + sync_entity_lifecycle(hass, DOMAIN, DOMAIN, component, yaml_collection, Schedule) storage_collection = ScheduleStorageCollection( Store( @@ -239,7 +237,7 @@ class ScheduleStorageCollection(StorageCollection): return data -class Schedule(Entity): +class Schedule(CollectionEntity): """Schedule entity.""" _attr_has_entity_name = True @@ -249,7 +247,7 @@ class Schedule(Entity): _next: datetime _unsub_update: Callable[[], None] | None = None - def __init__(self, config: ConfigType, editable: bool = True) -> None: + def __init__(self, config: ConfigType, editable: bool) -> None: """Initialize a schedule.""" self._config = ENTITY_SCHEMA(config) self._attr_capability_attributes = {ATTR_EDITABLE: editable} @@ -257,9 +255,15 @@ class Schedule(Entity): self._attr_name = self._config[CONF_NAME] self._attr_unique_id = self._config[CONF_ID] + @classmethod + def from_storage(cls, config: ConfigType) -> Schedule: + """Return entity instance initialized from storage.""" + schedule = cls(config, editable=True) + return schedule + @classmethod def from_yaml(cls, config: ConfigType) -> Schedule: - """Return entity instance initialized from yaml storage.""" + """Return entity instance initialized from yaml.""" schedule = cls(config, editable=False) schedule.entity_id = f"{DOMAIN}.{config[CONF_ID]}" return schedule @@ -291,14 +295,17 @@ class Schedule(Entity): todays_schedule = self._config.get(WEEKDAY_TO_CONF[now.weekday()], []) # Determine current schedule state - self._attr_state = next( - ( - STATE_ON - for time_range in todays_schedule - if time_range[CONF_FROM] <= now.time() <= time_range[CONF_TO] - ), - STATE_OFF, - ) + for time_range in todays_schedule: + # The current time should be greater or equal to CONF_FROM. + if now.time() < time_range[CONF_FROM]: + continue + # The current time should be smaller (and not equal) to CONF_TO. + # Note that any time in the day is treated as smaller than time.max. + if now.time() < time_range[CONF_TO] or time_range[CONF_TO] == time.max: + self._attr_state = STATE_ON + break + else: + self._attr_state = STATE_OFF # Find next event in the schedule, loop over each day (starting with # the current day) until the next event has been found. @@ -319,11 +326,15 @@ class Schedule(Entity): if next_event := next( ( possible_next_event - for time in times + for timestamp in times if ( possible_next_event := ( - datetime.combine(now.date(), time, tzinfo=now.tzinfo) + datetime.combine(now.date(), timestamp, tzinfo=now.tzinfo) + timedelta(days=day) + if not timestamp == time.max + # Special case for midnight of the following day. + else datetime.combine(now.date(), time(), tzinfo=now.tzinfo) + + timedelta(days=day + 1) ) ) > now diff --git a/homeassistant/components/schedule/translations/bg.json b/homeassistant/components/schedule/translations/bg.json new file mode 100644 index 00000000000..292b4186ce8 --- /dev/null +++ b/homeassistant/components/schedule/translations/bg.json @@ -0,0 +1,3 @@ +{ + "title": "\u0413\u0440\u0430\u0444\u0438\u043a" +} \ No newline at end of file diff --git a/homeassistant/components/schedule/translations/it.json b/homeassistant/components/schedule/translations/it.json index c50d8a66d1c..f75cfe395c6 100644 --- a/homeassistant/components/schedule/translations/it.json +++ b/homeassistant/components/schedule/translations/it.json @@ -5,5 +5,5 @@ "on": "Acceso" } }, - "title": "Programma" + "title": "Calendarizzazione" } \ No newline at end of file diff --git a/homeassistant/components/schedule/translations/nl.json b/homeassistant/components/schedule/translations/nl.json index ea585424fdb..38c6968ce14 100644 --- a/homeassistant/components/schedule/translations/nl.json +++ b/homeassistant/components/schedule/translations/nl.json @@ -4,5 +4,6 @@ "off": "Uit", "on": "Aan" } - } + }, + "title": "Schema" } \ No newline at end of file diff --git a/homeassistant/components/schedule/translations/pt.json b/homeassistant/components/schedule/translations/pt.json new file mode 100644 index 00000000000..b3fab8f544d --- /dev/null +++ b/homeassistant/components/schedule/translations/pt.json @@ -0,0 +1,3 @@ +{ + "title": "Hor\u00e1rio" +} \ No newline at end of file diff --git a/homeassistant/components/schedule/translations/sv.json b/homeassistant/components/schedule/translations/sv.json new file mode 100644 index 00000000000..04cd29ef2eb --- /dev/null +++ b/homeassistant/components/schedule/translations/sv.json @@ -0,0 +1,9 @@ +{ + "state": { + "_": { + "off": "Av", + "on": "P\u00e5" + } + }, + "title": "Schema" +} \ No newline at end of file diff --git a/homeassistant/components/schluter/climate.py b/homeassistant/components/schluter/climate.py index 3ccea458960..24a09437d4b 100644 --- a/homeassistant/components/schluter/climate.py +++ b/homeassistant/components/schluter/climate.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any from requests import RequestException import voluptuous as vol @@ -11,8 +12,6 @@ from homeassistant.components.climate import ( SCAN_INTERVAL, TEMP_CELSIUS, ClimateEntity, -) -from homeassistant.components.climate.const import ( ClimateEntityFeature, HVACAction, HVACMode, @@ -131,7 +130,7 @@ class SchluterThermostat(CoordinatorEntity, ClimateEntity): async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Mode is always heating, so do nothing.""" - def set_temperature(self, **kwargs): + def set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" target_temp = None target_temp = kwargs.get(ATTR_TEMPERATURE) diff --git a/homeassistant/components/scrape/translations/bg.json b/homeassistant/components/scrape/translations/bg.json index f22c3fd3b26..89c2ffc7880 100644 --- a/homeassistant/components/scrape/translations/bg.json +++ b/homeassistant/components/scrape/translations/bg.json @@ -11,6 +11,7 @@ "index": "\u0418\u043d\u0434\u0435\u043a\u0441", "name": "\u0418\u043c\u0435", "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "select": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435", "unit_of_measurement": "\u0415\u0434\u0438\u043d\u0438\u0446\u0430 \u0437\u0430 \u0438\u0437\u043c\u0435\u0440\u0432\u0430\u043d\u0435", "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" } @@ -26,7 +27,9 @@ "index": "\u0418\u043d\u0434\u0435\u043a\u0441", "name": "\u0418\u043c\u0435", "password": "\u041f\u0430\u0440\u043e\u043b\u0430", - "unit_of_measurement": "\u0415\u0434\u0438\u043d\u0438\u0446\u0430 \u0437\u0430 \u0438\u0437\u043c\u0435\u0440\u0432\u0430\u043d\u0435" + "select": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435", + "unit_of_measurement": "\u0415\u0434\u0438\u043d\u0438\u0446\u0430 \u0437\u0430 \u0438\u0437\u043c\u0435\u0440\u0432\u0430\u043d\u0435", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" } } } diff --git a/homeassistant/components/scrape/translations/cs.json b/homeassistant/components/scrape/translations/cs.json index 8669b5b1330..6c5458481b8 100644 --- a/homeassistant/components/scrape/translations/cs.json +++ b/homeassistant/components/scrape/translations/cs.json @@ -1,9 +1,14 @@ { "config": { + "abort": { + "already_configured": "\u00da\u010det je ji\u017e nastaven" + }, "step": { "user": { "data": { - "headers": "Hlavi\u010dky" + "headers": "Hlavi\u010dky", + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" } } } diff --git a/homeassistant/components/screenlogic/climate.py b/homeassistant/components/screenlogic/climate.py index adc103fa684..4b28eea7208 100644 --- a/homeassistant/components/screenlogic/climate.py +++ b/homeassistant/components/screenlogic/climate.py @@ -1,11 +1,12 @@ """Support for a ScreenLogic heating device.""" import logging +from typing import Any from screenlogicpy.const import DATA as SL_DATA, EQUIPMENT, HEAT_MODE -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( +from homeassistant.components.climate import ( ATTR_PRESET_MODE, + ClimateEntity, ClimateEntityFeature, HVACAction, HVACMode, @@ -130,7 +131,7 @@ class ScreenLogicClimate(ScreenlogicEntity, ClimateEntity, RestoreEntity): HEAT_MODE.NAME_FOR_NUM[mode_num] for mode_num in self._configured_heat_modes ] - async def async_set_temperature(self, **kwargs) -> None: + async def async_set_temperature(self, **kwargs: Any) -> None: """Change the setpoint of the heater.""" if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None: raise ValueError(f"Expected attribute {ATTR_TEMPERATURE}") @@ -144,7 +145,7 @@ class ScreenLogicClimate(ScreenlogicEntity, ClimateEntity, RestoreEntity): f"Failed to set_temperature {temperature} on body {self.body['body_type']['value']}" ) - async def async_set_hvac_mode(self, hvac_mode) -> None: + async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set the operation mode.""" if hvac_mode == HVACMode.OFF: mode = HEAT_MODE.OFF @@ -172,7 +173,7 @@ class ScreenLogicClimate(ScreenlogicEntity, ClimateEntity, RestoreEntity): f"Failed to set_preset_mode {mode} on body {self.body['body_type']['value']}" ) - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Run when entity is about to be added.""" await super().async_added_to_hass() diff --git a/homeassistant/components/script/__init__.py b/homeassistant/components/script/__init__.py index 53bd256c624..0effdc2754b 100644 --- a/homeassistant/components/script/__init__.py +++ b/homeassistant/components/script/__init__.py @@ -8,6 +8,7 @@ from typing import Any, cast import voluptuous as vol from voluptuous.humanize import humanize_error +from homeassistant.components import websocket_api from homeassistant.components.blueprint import CONF_USE_BLUEPRINT, BlueprintInputs from homeassistant.const import ( ATTR_ENTITY_ID, @@ -28,7 +29,7 @@ from homeassistant.const import ( STATE_ON, ) from homeassistant.core import HomeAssistant, ServiceCall, callback -from homeassistant.helpers import extract_domain_configs +from homeassistant.helpers import entity_registry as er, extract_domain_configs import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import make_entity_service_schema from homeassistant.helpers.entity import ToggleEntity @@ -79,91 +80,69 @@ def is_on(hass, entity_id): return hass.states.is_state(entity_id, STATE_ON) -@callback -def scripts_with_entity(hass: HomeAssistant, entity_id: str) -> list[str]: - """Return all scripts that reference the entity.""" +def _scripts_with_x( + hass: HomeAssistant, referenced_id: str, property_name: str +) -> list[str]: + """Return all scripts that reference the x.""" if DOMAIN not in hass.data: return [] - component = hass.data[DOMAIN] + component: EntityComponent[ScriptEntity] = hass.data[DOMAIN] return [ script_entity.entity_id for script_entity in component.entities - if entity_id in script_entity.script.referenced_entities + if referenced_id in getattr(script_entity.script, property_name) ] +def _x_in_script(hass: HomeAssistant, entity_id: str, property_name: str) -> list[str]: + """Return all x in a script.""" + if DOMAIN not in hass.data: + return [] + + component: EntityComponent[ScriptEntity] = hass.data[DOMAIN] + + if (script_entity := component.get_entity(entity_id)) is None: + return [] + + return list(getattr(script_entity.script, property_name)) + + +@callback +def scripts_with_entity(hass: HomeAssistant, entity_id: str) -> list[str]: + """Return all scripts that reference the entity.""" + return _scripts_with_x(hass, entity_id, "referenced_entities") + + @callback def entities_in_script(hass: HomeAssistant, entity_id: str) -> list[str]: """Return all entities in script.""" - if DOMAIN not in hass.data: - return [] - - component = hass.data[DOMAIN] - - if (script_entity := component.get_entity(entity_id)) is None: - return [] - - return list(script_entity.script.referenced_entities) + return _x_in_script(hass, entity_id, "referenced_entities") @callback def scripts_with_device(hass: HomeAssistant, device_id: str) -> list[str]: """Return all scripts that reference the device.""" - if DOMAIN not in hass.data: - return [] - - component = hass.data[DOMAIN] - - return [ - script_entity.entity_id - for script_entity in component.entities - if device_id in script_entity.script.referenced_devices - ] + return _scripts_with_x(hass, device_id, "referenced_devices") @callback def devices_in_script(hass: HomeAssistant, entity_id: str) -> list[str]: """Return all devices in script.""" - if DOMAIN not in hass.data: - return [] - - component = hass.data[DOMAIN] - - if (script_entity := component.get_entity(entity_id)) is None: - return [] - - return list(script_entity.script.referenced_devices) + return _x_in_script(hass, entity_id, "referenced_devices") @callback def scripts_with_area(hass: HomeAssistant, area_id: str) -> list[str]: """Return all scripts that reference the area.""" - if DOMAIN not in hass.data: - return [] - - component = hass.data[DOMAIN] - - return [ - script_entity.entity_id - for script_entity in component.entities - if area_id in script_entity.script.referenced_areas - ] + return _scripts_with_x(hass, area_id, "referenced_areas") @callback def areas_in_script(hass: HomeAssistant, entity_id: str) -> list[str]: """Return all areas in a script.""" - if DOMAIN not in hass.data: - return [] - - component = hass.data[DOMAIN] - - if (script_entity := component.get_entity(entity_id)) is None: - return [] - - return list(script_entity.script.referenced_areas) + return _x_in_script(hass, entity_id, "referenced_areas") @callback @@ -172,7 +151,7 @@ def scripts_with_blueprint(hass: HomeAssistant, blueprint_path: str) -> list[str if DOMAIN not in hass.data: return [] - component = hass.data[DOMAIN] + component: EntityComponent[ScriptEntity] = hass.data[DOMAIN] return [ script_entity.entity_id @@ -183,7 +162,7 @@ def scripts_with_blueprint(hass: HomeAssistant, blueprint_path: str) -> list[str async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Load the scripts from the configuration.""" - hass.data[DOMAIN] = component = EntityComponent(LOGGER, DOMAIN, hass) + hass.data[DOMAIN] = component = EntityComponent[ScriptEntity](LOGGER, DOMAIN, hass) # Process integration platforms right away since # we will create entities before firing EVENT_COMPONENT_LOADED @@ -205,9 +184,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def turn_on_service(service: ServiceCall) -> None: """Call a service to turn script on.""" variables = service.data.get(ATTR_VARIABLES) - script_entities: list[ScriptEntity] = cast( - list[ScriptEntity], await component.async_extract_from_service(service) - ) + script_entities = await component.async_extract_from_service(service) for script_entity in script_entities: await script_entity.async_turn_on( variables=variables, context=service.context, wait=False @@ -216,9 +193,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def turn_off_service(service: ServiceCall) -> None: """Cancel a script.""" # Stopping a script is ok to be done in parallel - script_entities: list[ScriptEntity] = cast( - list[ScriptEntity], await component.async_extract_from_service(service) - ) + script_entities = await component.async_extract_from_service(service) if not script_entities: return @@ -232,9 +207,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def toggle_service(service: ServiceCall) -> None: """Toggle a script.""" - script_entities: list[ScriptEntity] = cast( - list[ScriptEntity], await component.async_extract_from_service(service) - ) + script_entities = await component.async_extract_from_service(service) for script_entity in script_entities: await script_entity.async_toggle(context=service.context, wait=False) @@ -250,6 +223,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: hass.services.async_register( DOMAIN, SERVICE_TOGGLE, toggle_service, schema=SCRIPT_TURN_ONOFF_SCHEMA ) + websocket_api.async_register_command(hass, websocket_config) return True @@ -265,7 +239,7 @@ async def _async_process_config(hass, config, component) -> bool: for config_key in extract_domain_configs(config, DOMAIN): conf: dict[str, dict[str, Any] | BlueprintInputs] = config[config_key] - for object_id, config_block in conf.items(): + for key, config_block in conf.items(): raw_blueprint_inputs = None raw_config = None @@ -292,35 +266,11 @@ async def _async_process_config(hass, config, component) -> bool: raw_config = cast(ScriptConfig, config_block).raw_config entities.append( - ScriptEntity( - hass, object_id, config_block, raw_config, raw_blueprint_inputs - ) + ScriptEntity(hass, key, config_block, raw_config, raw_blueprint_inputs) ) await component.async_add_entities(entities) - async def service_handler(service: ServiceCall) -> None: - """Execute a service call to script.